simonbence commented on a change in pull request #5356:
URL: https://github.com/apache/nifi/pull/5356#discussion_r702859141



##########
File path: nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
##########
@@ -216,6 +219,9 @@ public static void main(String[] args) throws IOException, 
InterruptedException
                 dumpFile = null;
                 verbose = false;
             }
+        } else if (cmd.equalsIgnoreCase("status-history")) {
+            statusHistoryDays = args[2];

Review comment:
       I think a check on the number of arguments (like in `dump` or 
`diagnostic`) would be good here. If not for else, but line 224 will end up in 
a NPE if the args[3] is null (and `ArrayOutOfBounds` if not enough args). 
Anyway, some "guarding" would be needed here.

##########
File path: nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
##########
@@ -275,6 +282,9 @@ public static void main(String[] args) throws IOException, 
InterruptedException
             case "env":
                 runNiFi.env();
                 break;
+            case "status-history":

Review comment:
       Minor: just by the nature of the things, it migh make sense to move this 
just below the `diagnostic`

##########
File path: nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
##########
@@ -185,13 +187,14 @@ private static void printUsage() {
     }
 
     public static void main(String[] args) throws IOException, 
InterruptedException {
-        if (args.length < 1 || args.length > 3) {
+        if (args.length < 1 || args.length > 4) {
             printUsage();
             return;
         }
 
         File dumpFile = null;
         boolean verbose = false;
+        String statusHistoryDays = null;

Review comment:
       I am not quite sure about how and when this is being parsed but I am not 
fund of the idea that a value which has a name suggests to be a number (days) 
is handled as string. This should be a number. Integer let's say and also 
checked to be a positive number, bigger than zero. Depending on your purposes, 
this might give you the opportunity to provide a default value as well.

##########
File path: nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
##########
@@ -729,6 +739,15 @@ public void dump(final File dumpFile) throws IOException {
         makeRequest(DUMP_CMD, null, dumpFile, "thread dump");
     }
 
+    /**

Review comment:
       If I understand it well, based on the `if` at the beginning of the 
`main` method, the file cannot be null. Also: I would find it more readable to 
use the usual JavaDoc format and handle arguments in their separate block.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/integration/Cluster.java
##########
@@ -141,8 +143,8 @@ public Node createNode() {
         final ExtensionDiscoveringManager extensionManager = new 
StandardExtensionDiscoveringManager();
         final FingerprintFactory fingerprintFactory = new 
FingerprintFactory(encryptor, extensionManager, sensitiveValueEncoder);
         final FlowElection flowElection = new 
PopularVoteFlowElection(flowElectionTimeoutMillis, TimeUnit.MILLISECONDS, 
flowElectionMaxNodes, fingerprintFactory);
-
-        final Node node = new Node(nifiProperties, extensionManager, 
flowElection);
+        final StatusHistoryRepository statusHistoryRepository = new 
VolatileComponentStatusRepository();

Review comment:
       Minor: as `VolatileComponentStatusRepository` has a constructor with 
`NiFiProperties` as argument (which you have here) it would might worth to call 
that constructor instead of the no-arg one

##########
File path: nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
##########
@@ -119,6 +119,7 @@
     public static final String DUMP_CMD = "DUMP";
     public static final String DIAGNOSTICS_CMD = "DIAGNOSTICS";
     public static final String IS_LOADED_CMD = "IS_LOADED";
+    public static final String STATUS_HISTORY_CMD = "STATUS_HISTORY";

Review comment:
       Next time please squash commits before publishing. Thanks!

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
##########
@@ -1172,22 +1175,6 @@ private ProvenanceRepository 
createProvenanceRepository(final NiFiProperties pro
         }
     }
 
-    private StatusHistoryRepository createStatusHistoryRepository() {

Review comment:
       Nice work, thanks for extracting this!

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/StatusHistoryRepositoryFactoryBean.java
##########
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.spring;
+
+import org.apache.nifi.controller.status.history.StatusHistoryRepository;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.nar.NarThreadContextClassLoader;
+import org.apache.nifi.util.NiFiProperties;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+public class StatusHistoryRepositoryFactoryBean implements 
FactoryBean<StatusHistoryRepository>, ApplicationContextAware {
+
+    private static final String DEFAULT_COMPONENT_STATUS_REPO_IMPLEMENTATION = 
"org.apache.nifi.controller.status.history.VolatileComponentStatusRepository";
+
+    private ApplicationContext applicationContext;
+    private NiFiProperties nifiProperties;
+    private ExtensionManager extensionManager;
+    private StatusHistoryRepository statusHistoryRepository;
+
+    @Override
+    public StatusHistoryRepository getObject() throws Exception {
+        nifiProperties = applicationContext.getBean("nifiProperties", 
NiFiProperties.class);
+        extensionManager = applicationContext.getBean("extensionManager", 
ExtensionManager.class);
+        final String implementationClassName = 
nifiProperties.getProperty(NiFiProperties.COMPONENT_STATUS_REPOSITORY_IMPLEMENTATION,
 DEFAULT_COMPONENT_STATUS_REPO_IMPLEMENTATION);
+        if (implementationClassName == null) {
+            throw new RuntimeException("Cannot create Status History 
Repository because the NiFi Properties is missing the following property: "
+                    + 
NiFiProperties.COMPONENT_STATUS_REPOSITORY_IMPLEMENTATION);
+        }
+
+        try {
+            statusHistoryRepository = 
NarThreadContextClassLoader.createInstance(extensionManager, 
implementationClassName, StatusHistoryRepository.class, nifiProperties);
+            statusHistoryRepository.start();

Review comment:
       Minor: if we want to go with the Spring-wibe I suggeset to use the 
`start` method as an _init_ method instead of calling it here. 
[Example](https://mkyong.com/spring/spring-init-method-and-destroy-method-example/)

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/StatusHistoryRepositoryFactoryBean.java
##########
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.spring;
+
+import org.apache.nifi.controller.status.history.StatusHistoryRepository;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.nar.NarThreadContextClassLoader;
+import org.apache.nifi.util.NiFiProperties;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+public class StatusHistoryRepositoryFactoryBean implements 
FactoryBean<StatusHistoryRepository>, ApplicationContextAware {
+
+    private static final String DEFAULT_COMPONENT_STATUS_REPO_IMPLEMENTATION = 
"org.apache.nifi.controller.status.history.VolatileComponentStatusRepository";
+
+    private ApplicationContext applicationContext;
+    private NiFiProperties nifiProperties;
+    private ExtensionManager extensionManager;
+    private StatusHistoryRepository statusHistoryRepository;
+
+    @Override
+    public StatusHistoryRepository getObject() throws Exception {
+        nifiProperties = applicationContext.getBean("nifiProperties", 
NiFiProperties.class);
+        extensionManager = applicationContext.getBean("extensionManager", 
ExtensionManager.class);
+        final String implementationClassName = 
nifiProperties.getProperty(NiFiProperties.COMPONENT_STATUS_REPOSITORY_IMPLEMENTATION,
 DEFAULT_COMPONENT_STATUS_REPO_IMPLEMENTATION);
+        if (implementationClassName == null) {
+            throw new RuntimeException("Cannot create Status History 
Repository because the NiFi Properties is missing the following property: "

Review comment:
       As now this is a Spring-specific code it might worth to check if 
throwing `BeanCreationException` would be better.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/StatusHistoryRepositoryFactoryBean.java
##########
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.spring;
+
+import org.apache.nifi.controller.status.history.StatusHistoryRepository;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.nar.NarThreadContextClassLoader;
+import org.apache.nifi.util.NiFiProperties;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+public class StatusHistoryRepositoryFactoryBean implements 
FactoryBean<StatusHistoryRepository>, ApplicationContextAware {

Review comment:
       Please add some description here

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/reporting/TestStandardReportingContext.java
##########
@@ -87,6 +90,8 @@ public void setup() {
         extensionManager = new StandardExtensionDiscoveringManager();
         extensionManager.discoverExtensions(systemBundle, 
Collections.emptySet());
 
+        statusHistoryRepository = new VolatileComponentStatusRepository();

Review comment:
       Same as in case of `TestFlowController`

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/TestFlowController.java
##########
@@ -136,6 +139,8 @@ public void setup() {
         extensionManager = new StandardExtensionDiscoveringManager();
         extensionManager.discoverExtensions(systemBundle, 
Collections.emptySet());
 
+        statusHistoryRepository = new VolatileComponentStatusRepository();

Review comment:
       As you have NiFi properties here, it is suggested to use the relevant 
constructor. It might be even better to use a mock, as in case of 
`flowFileEventRepo` or `auditService`.

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/scheduling/ProcessorLifecycleIT.java
##########
@@ -557,10 +559,12 @@ private FlowManagerAndSystemBundle 
buildFlowControllerForTest(final String propK
         final ExtensionDiscoveringManager extensionManager = new 
StandardExtensionDiscoveringManager();
         extensionManager.discoverExtensions(systemBundle, 
Collections.emptySet());
 
+        final StatusHistoryRepository statusHistoryRepository = new 
VolatileComponentStatusRepository();

Review comment:
       Same as in case of `TestFlowController`

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/integration/FrameworkIntegrationTest.java
##########
@@ -231,6 +232,8 @@ protected final void initialize(final NiFiProperties 
nifiProperties) throws IOEx
         systemBundle = SystemBundle.create(nifiProperties);
         extensionManager.discoverExtensions(systemBundle, 
Collections.emptySet());
 
+        statusHistoryRepository = new VolatileComponentStatusRepository();

Review comment:
       Same as in case of `TestFlowController`

##########
File path: nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
##########
@@ -175,7 +176,8 @@ private static void printUsage() {
         System.out.println("Status : Determine if there is a running instance 
of Apache NiFi");
         System.out.println("Dump : Write a Thread Dump to the file specified 
by [options], or to the log if no file is given");
         System.out.println("Diagnostics : Write diagnostic information to the 
file specified by [options], or to the log if no file is given. The --verbose 
flag may be provided as an option before " +
-            "the filename, which may result in additional diagnostic 
information being written.");
+                "the filename, which may result in additional diagnostic 
information being written.");
+        System.out.println("Status-history : Save the status history to the 
file specified by [options], or to the log if no file is given");

Review comment:
       Please specify, which log (bootstrap)

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/BootstrapListener.java
##########
@@ -244,6 +245,12 @@ public void run() {
                                         sendAnswer(socket.getOutputStream(), 
answer);
                                         logger.debug("Responded to IS_LOADED 
request from Bootstrap with value: " + answer);
                                         break;
+                                    case STATUS_HISTORY:

Review comment:
       Minor: I know that the order of the commands is not kept consistently 
everywhere but in general I think we should try to keep it as much as possible. 
So I would suggest to move this just right after diagnostic.

##########
File path: nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
##########
@@ -185,13 +187,14 @@ private static void printUsage() {
     }
 
     public static void main(String[] args) throws IOException, 
InterruptedException {
-        if (args.length < 1 || args.length > 3) {
+        if (args.length < 1 || args.length > 4) {

Review comment:
       If my understanding is correct and the `status-history` supports two 
arguments, this should not be changed

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
##########
@@ -1263,6 +1276,29 @@ public DecommissionTask getDecommissionTask() {
         return decommissionTask;
     }
 
+    @Override
+    public String getNodeStatusHistoryJson(final int days) {
+        final Date now = new Date();
+        final Calendar calendar = Calendar.getInstance();
+        calendar.setTime(now);
+        calendar.add(Calendar.DATE, days);
+        final Date daysBefore = calendar.getTime();
+        final StatusHistory nodeStatusHistory = 
statusHistoryRepository.getNodeStatusHistory(daysBefore, now);
+
+        final ObjectMapper objectMapper = new ObjectMapper();
+        final DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
+        
prettyPrinter.indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE);
+
+        final StatusHistoryDTO statusHistoryDTO = 
StatusHistoryUtil.createStatusHistoryDTO(nodeStatusHistory);
+        try {
+            return 
objectMapper.writer(prettyPrinter).writeValueAsString(statusHistoryDTO);

Review comment:
       Moving the whole dataset (possibly multiple days of verbose JSON data) 
into a String looks like a kind of waste. If this method ends up returning some 
kind of marshalled data instead of a `StatusHistoryDTO`, it would be preferred 
to use the `writer.writeValue(OutputStream, Object)` method instead and 
returning with the `OutputStream`

##########
File path: nifi-server-api/src/main/java/org/apache/nifi/NiFiServer.java
##########
@@ -40,4 +40,6 @@
     DiagnosticsFactory getThreadDumpFactory();
 
     DecommissionTask getDecommissionTask();
+
+    String getNodeStatusHistoryJson(final int days);

Review comment:
       I find this method alien to this contract. This is a foundational 
contract of the NiFi and should be as narrow as it is possible. I am not sure 
about what would be the exact solution, but I see two opportunities. Ordered 
based on my personal preference:
   
   1./
   Use the "Interface Segregation Principle" and create a new (partial) 
interface which would have this new method. Something like `StatusProvider` 
(not necessary the perfect name, but I would not go neither too generic or 
narrow). Relevant classes might implement this interface as well (The best 
would be if only `JettyServer` would need to do this, not `HeadlessNiFiServer` 
or `MiNiFiServer`) the same way as now. The given instance should be exposed to 
the client (`BootstrapListener`) via this interface.
   
   2./
   In case the one above is not reachable, I would suggest to hook on the 
`getDiagnosticsFactory` .
   
   Anyway, the main point would be to not dilute the core interface and expose 
as less as it is possible.

##########
File path: nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
##########
@@ -275,6 +282,9 @@ public static void main(String[] args) throws IOException, 
InterruptedException
             case "env":
                 runNiFi.env();
                 break;
+            case "status-history":

Review comment:
       - Calling with no arguments results `ArrayIndexOutOfBoundsException` 
(looking for item at place 2)
   - Calling with 1 argument results `ArrayIndexOutOfBoundsException` (looking 
for item at place 2)
   - Calling with 2 arguments results `ArrayIndexOutOfBoundsException` (looking 
for 3)
   - Calling with 3 arguments results run without error message but no result 
(freshly built nifi, no data)
   - Calling with 3 arguments results run with a NiFi running: contrary to my 
expectations, not the first but the second argument is the number of days and 
not the second but the third argument is the file.
   
   Besides of the argument  handling needs to be fixed (or clarified), I would 
suggest to provide some information for the user about what is going on. (Like 
the number of expected arguments; no running NiFi to execute against or alike)

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
##########
@@ -1263,6 +1276,29 @@ public DecommissionTask getDecommissionTask() {
         return decommissionTask;
     }
 
+    @Override
+    public String getNodeStatusHistoryJson(final int days) {

Review comment:
       - If the dependencies allow that, I would prefer to return with a 
`StatusHistoryDTO` (which is part of the API) and let `BootstrapListener` 
decide if it needs this in JSON format
   - Regardless of the potential change above, I find it strange in this 
abstraction level (NiFiServer) to have a method like this. Neither the name or 
the placement seems smooth to me. (Especially the "Json" remark) - Please see 
`NiFiServer` as well.

##########
File path: nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
##########
@@ -275,6 +282,9 @@ public static void main(String[] args) throws IOException, 
InterruptedException
             case "env":
                 runNiFi.env();
                 break;
+            case "status-history":

Review comment:
       One more question: in case of the file already exists, the original file 
is overwritten silently. I am not sure this is the right behaviour. Could we at 
least inform the user that the original will be overwritten?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/spring/StatusHistoryRepositoryFactoryBean.java
##########
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.spring;
+
+import org.apache.nifi.controller.status.history.StatusHistoryRepository;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.nar.NarThreadContextClassLoader;
+import org.apache.nifi.util.NiFiProperties;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+public class StatusHistoryRepositoryFactoryBean implements 
FactoryBean<StatusHistoryRepository>, ApplicationContextAware {
+
+    private static final String DEFAULT_COMPONENT_STATUS_REPO_IMPLEMENTATION = 
"org.apache.nifi.controller.status.history.VolatileComponentStatusRepository";
+
+    private ApplicationContext applicationContext;
+    private NiFiProperties nifiProperties;
+    private ExtensionManager extensionManager;
+    private StatusHistoryRepository statusHistoryRepository;
+
+    @Override
+    public StatusHistoryRepository getObject() throws Exception {
+        nifiProperties = applicationContext.getBean("nifiProperties", 
NiFiProperties.class);
+        extensionManager = applicationContext.getBean("extensionManager", 
ExtensionManager.class);
+        final String implementationClassName = 
nifiProperties.getProperty(NiFiProperties.COMPONENT_STATUS_REPOSITORY_IMPLEMENTATION,
 DEFAULT_COMPONENT_STATUS_REPO_IMPLEMENTATION);
+        if (implementationClassName == null) {
+            throw new RuntimeException("Cannot create Status History 
Repository because the NiFi Properties is missing the following property: "
+                    + 
NiFiProperties.COMPONENT_STATUS_REPOSITORY_IMPLEMENTATION);
+        }
+
+        try {
+            statusHistoryRepository = 
NarThreadContextClassLoader.createInstance(extensionManager, 
implementationClassName, StatusHistoryRepository.class, nifiProperties);
+            statusHistoryRepository.start();
+            return statusHistoryRepository;
+        } catch (final Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public Class<?> getObjectType() {
+        return StatusHistoryRepository.class;
+    }
+
+    @Override
+    public boolean isSingleton() {

Review comment:
       `FactoryBean` has a default implementation, returning `true`. I am not 
sure this is needed.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to