Skip to content
Snippets Groups Projects
Commit f10bc1d0 authored by xiaohua guan's avatar xiaohua guan Committed by Yas Naoi
Browse files

Issue #3284774 by Xiaohua Guan, yas, baldwinlouie: Deploy Cloud Orchestrator...

Issue #3284774 by Xiaohua Guan, yas, baldwinlouie: Deploy Cloud Orchestrator by Cloud Cluster (by CFn) (Deploy)
parent 38cb9a1c
Branches 2497145-front-page
No related tags found
No related merge requests found
Showing
with 609 additions and 44 deletions
......@@ -23,6 +23,10 @@ services:
class: Drupal\aws_cloud\Service\CloudWatch\CloudWatchService
arguments: ['@config.factory', '@plugin.manager.cloud_config_plugin']
aws_cloud.cloud_formation:
class: Drupal\aws_cloud\Service\CloudFormation\CloudFormationService
arguments: ['@config.factory', '@plugin.manager.cloud_config_plugin']
aws_cloud.instance_type_price_data_provider:
class: Drupal\aws_cloud\Service\Pricing\InstanceTypePriceDataProvider
arguments: ['@aws_cloud.pricing']
......@@ -59,3 +63,7 @@ services:
aws_cloud.ec2_operations:
class: Drupal\aws_cloud\Service\Ec2\Ec2OperationsService
arguments: ['@entity_type.manager', '@aws_cloud.ec2']
aws_cloud.cloud_orchestrator_manager:
class: Drupal\aws_cloud\Service\AwsCloudCloudOrchestratorManager
arguments: ['@aws_cloud.cloud_formation', '@entity_field.manager', '@entity_type.manager']
<?php
namespace Drupal\aws_cloud\Service;
use Drupal\aws_cloud\Service\CloudFormation\CloudFormationServiceInterface;
use Drupal\cloud\Service\CloudOrchestratorManagerInterface;
use Drupal\cloud\Traits\CloudContentEntityTrait;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* AWS Cloud cloud orchestrator manager.
*/
class AwsCloudCloudOrchestratorManager implements CloudOrchestratorManagerInterface {
use CloudContentEntityTrait;
/**
* The CloudFormation Service.
*
* @var \Drupal\aws_cloud\Service\CloudFormation\CloudFormationServiceInterface
*/
protected $cloudFormationService;
/**
* Entity field manager interface.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The Entity Type Manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The AwsCloudCloudOrchestratorManager constructor.
*
* @param \Drupal\aws_cloud\Service\CloudFormation\CloudFormationServiceInterface $cloud_formation_service
* The CloudFormation Service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(
CloudFormationServiceInterface $cloud_formation_service,
EntityFieldManagerInterface $entity_field_manager,
EntityTypeManagerInterface $entity_type_manager) {
$this->cloudFormationService = $cloud_formation_service;
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_type_manager;
$this->messenger();
}
/**
* {@inheritdoc}
*/
public function deploy($cloud_context, $manifest, $parameters): void {
$this->cloudFormationService->setCloudContext($cloud_context);
$stack_name = $parameters['StackName'];
unset($parameters['StackName']);
$cloud_formation_params = [];
foreach ($parameters as $key => $value) {
$cloud_formation_params[] = [
'ParameterKey' => $key,
'ParameterValue' => $value,
];
}
$this->cloudFormationService->createStack([
'StackName' => $stack_name,
'Capabilities' => [
'CAPABILITY_NAMED_IAM',
'CAPABILITY_AUTO_EXPAND',
],
'TemplateBody' => $manifest,
'Parameters' => $cloud_formation_params,
]);
}
}
<?php
namespace Drupal\aws_cloud\Service\CloudFormation;
use Aws\Credentials\AssumeRoleCredentialProvider;
use Aws\Credentials\CredentialProvider;
use Aws\MockHandler;
use Aws\Result;
use Aws\ResultInterface;
use Aws\CloudFormation\Exception\CloudFormationException;
use Aws\CloudFormation\CloudFormationClient;
use Aws\Sts\StsClient;
use Drupal\cloud\Plugin\cloud\config\CloudConfigPluginManagerInterface;
use Drupal\cloud\Service\CloudServiceBase;
use Drupal\Core\Config\ConfigFactoryInterface;
/**
* Interacts with the AWS Cloud CloudFormation API.
*/
class CloudFormationService extends CloudServiceBase implements CloudFormationServiceInterface {
/**
* Cloud context string.
*
* @var string
*/
private $cloudContext;
/**
* The config factory.
*
* Subclasses should use the self::config() method, which may be overridden to
* address specific needs when loading config, rather than this property
* directly. See \Drupal\Core\Form\ConfigFormBase::config() for an example of
* this.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The cloud service provider plugin manager (CloudConfigPluginManager).
*
* @var \Drupal\cloud\Plugin\cloud\config\CloudConfigPluginManagerInterface
*/
private $cloudConfigPluginManager;
/**
* TRUE or FALSE whether to be in test mode.
*
* @var bool
*/
private $testMode;
/**
* Constructs a new CloudFormationService object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* A configuration factory.
* @param \Drupal\cloud\Plugin\cloud\config\CloudConfigPluginManagerInterface $cloud_config_plugin_manager
* The cloud service provider plugin manager (CloudConfigPluginManager).
*/
public function __construct(ConfigFactoryInterface $config_factory,
CloudConfigPluginManagerInterface $cloud_config_plugin_manager) {
// The parent constructor takes care of $this->messenger object.
parent::__construct();
// Setup the configuration factory.
$this->configFactory = $config_factory;
// Setup the testMode flag.
$this->testMode = (bool) $this->configFactory->get('aws_cloud.settings')->get('aws_cloud_test_mode');
$this->cloudConfigPluginManager = $cloud_config_plugin_manager;
}
/**
* {@inheritdoc}
*/
public function setCloudContext($cloud_context): void {
$this->cloudContext = $cloud_context;
$this->cloudConfigPluginManager->setCloudContext($cloud_context);
}
/**
* {@inheritdoc}
*/
public function createStack(array $params = []): ?ResultInterface {
$results = $this->execute('CreateStack', $params);
return $results;
}
/**
* Execute the API of AWS Cloud CloudFormation service.
*
* @param string $operation
* The operation to perform.
* @param array $params
* An array of parameters.
*
* @return \Aws\ResultInterface
* Result object or NULL if there is an error.
*
* @throws \Drupal\aws_cloud\Service\CloudFormation\CloudFormationServiceException
* If the $cloud_formation_client (CloudFormationClient) is NULL.
*/
private function execute(string $operation, array $params = []): ?ResultInterface {
$results = NULL;
$cloud_formation_client = $this->getCloudFormationClient();
if ($cloud_formation_client === NULL) {
throw new CloudFormationServiceException('No CloudFormation Client found. Cannot perform API operations');
}
try {
// Let other modules alter the parameters
// before they are sent through the API.
\Drupal::moduleHandler()->invokeAll('aws_cloud_pre_execute_alter', [
&$params,
$operation,
$this->cloudContext,
]);
$command = $cloud_formation_client->getCommand($operation, $params);
$results = $cloud_formation_client->execute($command);
// Let other modules alter the results before the module processes it.
\Drupal::moduleHandler()->invokeAll('aws_cloud_post_execute_alter', [
&$results,
$operation,
$this->cloudContext,
]);
}
catch (CloudFormationException $e) {
// IAM permission validation needs AwsException to be thrown as it is
// determined by the AwsErrorCode of the exception, 'DryRunOperation'.
if (!empty($params['DryRun'])) {
throw $e;
}
$this->messenger->addError($this->t('Error: The operation "@operation" could not be performed.', [
'@operation' => $operation,
]));
$this->messenger->addError($this->t('Error Info: @error_info', [
'@error_info' => $e->getAwsErrorCode(),
]));
$this->messenger->addError($this->t('Error from: @error_type-side', [
'@error_type' => $e->getAwsErrorType(),
]));
$this->messenger->addError($this->t('Status Code: @status_code', [
'@status_code' => $e->getStatusCode(),
]));
$this->messenger->addError($this->t('Message: @msg', ['@msg' => $e->getAwsErrorMessage()]));
throw $e;
}
catch (\Exception $e) {
$this->handleException($e);
throw $e;
}
return $results;
}
/**
* Load and return a CloudFormationClient.
*/
private function getCloudFormationClient(): ?CloudFormationClient {
// Use the plugin manager to load the aws credentials.
$credentials = $this->cloudConfigPluginManager->loadCredentials();
try {
$cloud_formation_params = [
'region' => $credentials['region'],
'version' => $credentials['version'],
'http' => [
'connect_timeout' => $this->connectTimeout,
],
];
$provider = FALSE;
// Load credentials if needed.
if (empty($credentials['use_instance_profile'])) {
$provider = CredentialProvider::ini('default', $credentials['ini_file']);
$provider = CredentialProvider::memoize($provider);
}
if (!empty($credentials['use_assume_role'])) {
// Assume role.
$sts_params = [
'region' => $credentials['region'],
'version' => $credentials['version'],
];
if ($provider !== FALSE) {
$sts_params['credentials'] = $provider;
}
$assumeRoleCredentials = new AssumeRoleCredentialProvider([
'client' => new StsClient($sts_params),
'assume_role_params' => [
'RoleArn' => $credentials['role_arn'],
'RoleSessionName' => 'cloud_formation_client_assume_role',
],
]);
// Memoize takes care of re-authenticating when the tokens expire.
$assumeRoleCredentials = CredentialProvider::memoize($assumeRoleCredentials);
$cloud_formation_params = [
'region' => $credentials['region'],
'version' => $credentials['version'],
'credentials' => $assumeRoleCredentials,
];
// If switch role is enabled, execute one more assume role.
if (!empty($credentials['use_switch_role'])) {
$switch_sts_params = [
'region' => $credentials['region'],
'version' => $credentials['version'],
'credentials' => $assumeRoleCredentials,
];
$switchRoleCredentials = new AssumeRoleCredentialProvider([
'client' => new StsClient($switch_sts_params),
'assume_role_params' => [
'RoleArn' => $credentials['switch_role_arn'],
'RoleSessionName' => 'cloud_formation_client_switch_role',
],
]);
$switchRoleCredentials = CredentialProvider::memoize($switchRoleCredentials);
$cloud_formation_params['credentials'] = $switchRoleCredentials;
}
}
elseif ($provider !== FALSE) {
$cloud_formation_params['credentials'] = $provider;
}
$cloud_formation_client = new CloudFormationClient($cloud_formation_params);
}
catch (\Exception $e) {
$cloud_formation_client = NULL;
$this->logger('cloud_formation_service')->error($e->getMessage());
}
if ($this->testMode) {
$this->addMockHandler($cloud_formation_client);
}
return $cloud_formation_client;
}
/**
* Add a mock handler of aws sdk for testing.
*
* The mock data of aws response is saved
* in configuration "aws_cloud_mock_data".
*
* @param \Aws\CloudFormation\CloudFormationClient $cloud_formation_client
* The CloudFormation client.
*
* @see https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_handlers-and-middleware.html
*/
private function addMockHandler(CloudFormationClient $cloud_formation_client): void {
$mock_data = $this->configFactory->get('aws_cloud.settings')->get('aws_cloud_mock_data');
if (empty($this->testMode) || empty($mock_data)) {
return;
}
$mock_data = json_decode($mock_data, TRUE);
$result = static function ($command, $request) use ($mock_data) {
$command_name = $command->getName();
$response_data = $mock_data[$command_name] ?? [];
// ErrorCode field is proprietary defined to mock AwsErrorCode.
// Checking the value of DryRun parameter as some test cases expect a
// different result based on the DryRun parameter.
if (!empty($response_data['ErrorCode'])
&& $command->hasParam('DryRun')
&& ($command->toArray())['DryRun']) {
return new CloudFormationException('CloudFormationException by CloudFormationService::addMockHandler()',
$command, ['code' => $response_data['ErrorCode']]);
}
return new Result($response_data);
};
// Set a mock handler with the number of mocked results in the queue.
// The mock queue count should be the number of API calls and is similar
// to the prepared mocked results. TO be safe, an additional queue is set
// for an async call or a repeated call.
$results_on_queue = array_pad([], count($mock_data) + 1, $result);
$cloud_formation_client
->getHandlerList()
->setHandler(new MockHandler($results_on_queue));
}
}
<?php
namespace Drupal\aws_cloud\Service\CloudFormation;
/**
* AWS Cloud CloudFormation service exception.
*/
class CloudFormationServiceException extends \Exception {
}
<?php
namespace Drupal\aws_cloud\Service\CloudFormation;
use Aws\ResultInterface;
/**
* Interacts with the AWS Cloud CloudFormation API.
*/
interface CloudFormationServiceInterface {
/**
* Set the cloud context.
*
* @param string $cloud_context
* Cloud context string.
*/
public function setCloudContext($cloud_context): void;
/**
* Calls the Amazon CloudFormation API endpoint CreateStack.
*
* @param array $params
* Parameters array to send to API.
*
* @return mixed
* An array of results or NULL if there is an error.
*/
public function createStack(array $params = []): ?ResultInterface;
}
......@@ -677,14 +677,29 @@ function cloud_cluster_form_cloud_launch_template_cloud_cluster_launch_form_alte
'#open' => TRUE,
];
$cloud_context = array_keys($cloud_contexts)[0];
$form['cloud_service_provider']['target_provider'] = [
'#type' => 'select',
'#title' => t('Target provider'),
'#options' => $cloud_contexts,
'#default_value' => $cloud_context,
'#required' => TRUE,
'#ajax' => [
'callback' => 'cloud_cluster_target_provider_ajax_callback',
'event' => 'change',
'wrapper' => 'deployment-parameters-container',
'progress' => [
'type' => 'throbber',
'message' => t('Retrieving...'),
],
],
];
cloud_cluster_render_deployment_parameters($form, $cloud_launch_template);
if (!empty($form_state->getValue('target_provider'))) {
$cloud_context = $form_state->getValue('target_provider');
}
cloud_cluster_render_deployment_parameters($form, $cloud_launch_template, $cloud_context);
$view_builder = \Drupal::entityTypeManager()->getViewBuilder('cloud_launch_template');
$build = $view_builder->view($cloud_launch_template, 'view');
......@@ -692,6 +707,21 @@ function cloud_cluster_form_cloud_launch_template_cloud_cluster_launch_form_alte
$form['detail'] = $build;
}
/**
* Ajax callback when the target provider dropdown changes.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state interface element.
*
* @return array
* Form element for parameters.
*/
function cloud_cluster_target_provider_ajax_callback(array $form, FormStateInterface $form_state): array {
return $form['parameters'];
}
/**
* Render form elements for deployment parameters.
*
......@@ -700,12 +730,15 @@ function cloud_cluster_form_cloud_launch_template_cloud_cluster_launch_form_alte
* @param \Drupal\cloud\Entity\CloudLaunchTemplate $cloud_launch_template
* The cloud launch template.
*/
function cloud_cluster_render_deployment_parameters(array &$form, CloudLaunchTemplate $cloud_launch_template) {
function cloud_cluster_render_deployment_parameters(array &$form, CloudLaunchTemplate $cloud_launch_template, $cloud_context) {
$detail = [
'#type' => 'details',
'#title' => t('Deployment Parameters'),
'#open' => TRUE,
'#tree' => TRUE,
'#attributes' => [
'id' => 'deployment-parameters-container',
],
];
$template_path = realpath(
......@@ -716,19 +749,27 @@ function cloud_cluster_render_deployment_parameters(array &$form, CloudLaunchTem
foreach ($config['groups'] ?: [] as $group) {
$detail[$group['name']] = [
'#type' => 'details',
'#title' => t('@label', ['@label' => $group['label']]),
'#title' => $group['title'],
'#open' => TRUE,
];
foreach ($group['parameters'] ?: [] as $parameter) {
$detail[$group['name']][$parameter['name']] = [
'#type' => $parameter['type'],
'#title' => t('@label', ['@label' => $parameter['label']]),
'#default_value' => $parameter['default_value'],
'#required' => TRUE,
];
}
$item = [];
foreach ($parameter as $key => $value) {
if ($key === 'name') {
continue;
}
if ($key === 'cloud_context') {
$value = $cloud_context;
}
$item["#$key"] = $value;
}
$detail[$group['name']][$parameter['name']] = $item;
}
}
$form['parameters'] = $detail;
......
<?php
namespace Drupal\cloud_cluster\Element;
use Drupal\Core\Render\Element\Select;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form element for entity selection box.
*
* @FormElement("entity_select")
*/
class EntitySelect extends Select {
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#process' => [
[static::class, 'processEntitySelect'],
[Select::class, 'processAjaxForm'],
],
] + parent::getInfo();
}
/**
* Processes a select list form element.
*
* @param array $element
* The form element to process.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*
* @see _form_validate()
*/
public static function processEntitySelect(array &$element, FormStateInterface $form_state, array &$complete_form): array {
$zones = \DateTimeZone::listIdentifiers();
// Load entities.
$entities = \Drupal::entityTypeManager()
->getStorage($element['#entity_type'])
->loadByProperties([
'cloud_context' => $element['#cloud_context'],
]);
$options = [];
foreach ($entities as $entity) {
$options[$entity->get($element['#entity_key'])->value] = $entity->label();
}
$element['#options'] = $options;
return Select::processSelect($element, $form_state, $complete_form);
}
}
......@@ -197,10 +197,6 @@ class CloudClusterCloudLaunchTemplatePlugin extends CloudPluginBase implements C
];
$deployment_type = $cloud_launch_template->field_deployment_type->value;
if ($deployment_type === 'aws_cloud') {
$this->messenger->addMessage(t('AWS cloud will be implemented.'));
return $route;
}
$template_definitions_path = realpath(
sprintf(
......@@ -215,7 +211,7 @@ class CloudClusterCloudLaunchTemplatePlugin extends CloudPluginBase implements C
$template_definitions = Yaml::decode(file_get_contents($template_definitions_path));
$template_definition = $template_definitions[$deployment_template];
$template_location = $template_definition['location'];
if (strpos($template_location, 'http://') === FALSE) {
if ($deployment_type === 'k8s') {
$template_location = dirname($template_definitions_path) . '/' . $template_location;
}
......@@ -240,16 +236,14 @@ class CloudClusterCloudLaunchTemplatePlugin extends CloudPluginBase implements C
}
try {
\Drupal::service($service_name)->deploy($form_state->getValue('target_provider'), $yaml);
\Drupal::service($service_name)->deploy($form_state->getValue('target_provider'), $yaml, $parameters);
$this->messenger->addMessage(t('Succeeded to deploy cloud orchestrator.'));
$cloud_launch_template->get('field_workflow_status')->setValue(CloudLaunchTemplateInterface::DRAFT);
$cloud_launch_template->validate();
$cloud_launch_template->save();
}
catch (\Exception $e) {
$this->messenger->addError(t('Failed to deploy cloud orchestrator due to: @exception', [
'@exception' => $e->getMessage(),
]));
$this->messenger->addError(t('Failed to deploy cloud orchestrator.'));
}
return $route;
......
groups:
- name: stack
title: Stack Configuration
parameters:
- name: StackName
title: Stack name
default_value: ''
type: textfield
- name: StackPrefix
title: Stack prefix
description: A prefix to append to resource names/IDs. For example, ${StackPrefix}-IAM-Role,
${StackPrefix}-Drupal-RDS for RDS DB Identifier. Must be between 1 and 20 characters
and only contain alphanumeric characters and hyphens.
default_value: ''
pattern: ^[a-zA-Z0-9\\-]+$
maxlength: 20
type: textfield
- name: drupal
label: Drupal configuration
title: Drupal configuration
parameters:
- name: DrupalUserName
label: Drupal administrator username
title: Drupal administrator username
default_value: cloud_admin
type: textfield
- name: DrupalPassword
label: Drupal administrator password
title: Drupal administrator password
description: >-
The Drupal admin account password. Must be between 6 and 32 characters
and only contain alphanumeric characters and these special characters ` ~ ! # $ % ^ & * ( ) _ + , . \ -
default_value: cloud_admin
pattern: ^[\w`~!#$%^&*()_+,.\\-]+$
maxlength: 32
type: password
- name: DrupalEmail
label: Drupal administrator email address
title: Drupal administrator email address
default_value: cloud_admin@example.com
type: email
- name: DrupalTimezone
label: Drupal default time zone
title: Drupal default time zone
default_value: America/Los_Angeles
type: timezone
- name: database
label: Database configuration
title: Database configuration
parameters:
- name: MySQLUserName
label: MySQL administrator username
title: MySQL administrator username
default_value: mysql_admin
type: textfield
- name: MySQLPassword
label: MySQL administrator password
title: MySQL administrator password
description: >-
Password for the RDS Username. Must be between 6 and 32 characters
and only contain alphanumeric characters and these special characters ` ~ ! # $ % ^ & * ( ) _ + , . \ -
default_value: mysql_admin_password
maxlength: 32
pattern: ^[\w`~!#$%^&*()_+,.\\-]+$
type: password
- name: DatabaseName
label: MySQL database name
title: MySQL database name
description: The name of the database. Must be between 4 and 32 characters and
only contain alphanumeric characters and underscores.
pattern: ^[a-zA-Z0-9_]+$
maxlength: 32
minlength: 4
default_value: cloud_orchestrator
type: textfield
- name: aws_ec2
label: Amazon EC2 Configuration
title: Amazon EC2 Configuration
parameters:
- name: KeyName
label: EC2 key name
title: EC2 key name
default_value: ''
type: textfield
type: entity_select
entity_type: aws_cloud_key_pair
entity_key: key_pair_name
cloud_context: ''
groups:
- name: drupal
label: Drupal configuration
title: Drupal configuration
parameters:
- name: drupal_user
label: Drupal administrator username
title: Drupal administrator username
default_value: cloud_admin
type: textfield
- name: drupal_password
label: Drupal administrator password
title: Drupal administrator password
default_value: cloud_admin
type: password
- name: drupal_email
label: Drupal administrator email address
title: Drupal administrator email address
default_value: cloud_admin@example.com
type: email
- name: drupal_timezone
label: Drupal default time zone
title: Drupal default time zone
default_value: America/Los_Angeles
type: timezone
- name: database
label: Database configuration
title: Database configuration
parameters:
- name: mysql_user
label: MySQL administrator username
title: MySQL administrator username
default_value: mysql_admin
type: textfield
- name: mysql_password
label: MySQL administrator password
title: MySQL administrator password
default_value: mysql_admin_password
type: password
- name: mysql_database
label: MySQL database name
title: MySQL database name
default_value: cloud_orchestrator
type: textfield
......@@ -61,10 +61,8 @@ class K8sCloudOrchestratorManager implements CloudOrchestratorManagerInterface {
/**
* {@inheritdoc}
*/
public function deploy($cloud_context, $manifest): void {
public function deploy($cloud_context, $manifest, $parameters): void {
$this->k8sService->setCloudContext($cloud_context);
$this->k8sService->supportedCloudLaunchTemplates();
$object_types = $this->k8sService->supportedCloudLaunchTemplates();
$yamls = $this->k8sService->decodeMultipleDocYaml($manifest);
......
......@@ -14,7 +14,9 @@ interface CloudOrchestratorManagerInterface {
* The target cloud service provider.
* @param string $manifest
* The manifest content to deploy orchestrator.
* @param array $parameters
* The parameters.
*/
public function deploy(string $cloud_context, string $manifest): void;
public function deploy(string $cloud_context, string $manifest, array $parameters): void;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment