Заметки

Реклама:

Оглавление

27.07.2017
Servers - Архивирование в AWS Glacier (часть 5)

В этой заметке рассказывается о базе данных для регистрации архивов, вспомогательных командных файлах и расписании запусков скриптов.

Скрипт для создания таблицы, в которую записывается информация об архивах:

USE [AWS-Glacier]
GO

/****** Object:  Table [dbo].[agc-prod]    Script Date: 7/27/2017 9:49:56 AM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[archive-2017](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [archive-name] [nvarchar](255) NULL,
    [aws-account-id] [nvarchar](255) NULL,
    [aws-vault-name] [nvarchar](255) NULL,
    [aws-archive-id] [nvarchar](255) NULL,
    [archive-content] [ntext] NULL,
    [creation-timestamp] [datetime] NULL,
    [last-access-timestamp] [datetime] NULL,
    [state] [nvarchar](255) NULL,
    [notice] [nvarchar](255) NULL,
 CONSTRAINT [PK_Glacier-Content] PRIMARY KEY CLUSTERED 
(
    [id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

Скрипт запуска скрипта архивирования и копирования в облако - AWS-1-BackupAndTransfer.cmd:

powershell.exe "%~DP0Process-AWSGlacierBackup-Prod.ps1" 
powershell.exe "%~DP0Process-AWSGlacierBackup-Serv.ps1"

Скрипт запуска скрипта инвентаризации - AWS-2-Inventory.cmd:

powershell.exe "%~DP0Process-AWSGlacierInventory-Prod.ps1"
powershell.exe "%~DP0Process-AWSGlacierInventory-Serv.ps1"

Скрипт запуска скрипта сверки архивов на сервере и в облаке - AWS-3-Checking.cmd:

powershell.exe "%~DP0Process-AWSGlacierChecking-Prod.ps1"
powershell.exe "%~DP0Process-AWSGlacierChecking-Serv.ps1"

Все три вышеописанных скрипта необходимо поставить в планировщик запуска задач на сервере. Я рекомендую запускать скрипты по такому расписанию:

  • Пятница после рабочего дня - AWS-1-BackupAndTransfer.cmd
  • Воскресенье в середине дня - AWS-2-Inventory.cmd
  • Понедельник перед рабочим днем - AWS-3-Checking.cmd

Все задания нужно запускать с повышенными привилегиями (RunLevel = HighestAvailable) и от имени системы NT AUTHORITY\SYSTEM (UserId = S-1-5-18).

 

25.07.2017
Servers - Архивирование в AWS Glacier (часть 4)

В этой заметке описывается скрипт для проверки содержимого облачного архива AWS Glacier и удаления локальных файлов, успешно размещенных в облаке.

Логика скрипта:

  • Найти файл с номером задания.
  • Подключиться к AWS Glacier и скачать результат инвентаризации коллекции архивов.
  • В базе данных компании найти все архивы с состоянием transfered.
  • Сравнить размеры локального архива и файла в облаке.
  • Удалить локальный архив и пометить его состояние в базе данных как archived.

Код скрипта:

# Folder for saving archives
$ArchiveFolder = "C:\2_Transfer"

# Notifications
$MailServer = "mail.domain.com"
$MailEncoding = [System.Text.Encoding]::UTF8
$MailFrom = "$env:computername@domain.com"
$MailSubject = "Glacier backup"
[string[]]$MailTO = "admin@domain.com"
[string[]]$MailCC = ""

# AWS variables
$AWSAccountID = 'AWS_Account_ID'
$AWSRegion = 'AWS_region'
$AWSVaultName = 'AWS_Vault_Name'
$AWSProfileAccessKey = "AWS_Access_Key"
$AWSProfileSecretKey = "AWS_Secret_Key"
$AWSJobIdFileName = "AWSGlacier-InventoryJobId.txt"

# SQL variables
$SQLServer = 'SQLServer.domain.com'
$SQLDatabase = 'AWS-Glacier'
$SQLTable = '[archive-2017]'
$SQLUsername = 'db_user'
$SQLPassword = 'db_password'

# Registering AWS libraries
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Core.dll"
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Glacier.dll"

Function Write-ScriptLog {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Message,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$LogFile
  )
  Process {
    $LogMessage = Get-Date -uformat "%d.%m.%Y %H:%M:%S"
    $LogMessage += "`t"
    $LogMessage += $Message
    $LogMessage | Out-File -FilePath $LogFile -Append
  }
}#End Function

Function Invoke-Query {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$ServerInstance,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Database,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Username,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Password,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Query
  )
  Process {
    $ConnectionString = "Server=$ServerInstance;uid=$Username;pwd=$Password;Database=$Database;Integrated Security=False;"
    $Connection = New-Object System.Data.SqlClient.SqlConnection
    $Connection.ConnectionString = $ConnectionString
    $Connection.Open()
    $Command = New-Object System.Data.SQLClient.SQLCommand
    $Command.Connection = $Connection
    $Command.CommandText = $Query
    $Result = $Command.ExecuteReader()
    $Datatable = New-Object “System.Data.DataTable”
    $Datatable.Load($Result)
    $Connection.Close()
    Return $Datatable
  }
}#End Function

Function Invoke-NonQuery {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$ServerInstance,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Database,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Username,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Password,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Query
  )
  Process {
    $ConnectionString = "Server=$ServerInstance;uid=$Username;pwd=$Password;Database=$Database;Integrated Security=False;"
    $Connection = New-Object System.Data.SqlClient.SqlConnection
    $Connection.ConnectionString = $ConnectionString
    $Connection.Open()
    $Command = New-Object System.Data.SQLClient.SQLCommand
    $Command.Connection = $Connection
    $Command.CommandText = $Query
    $Result = $Command.ExecuteNonQuery()
    $Connection.Close()
  }
}#End Function

# --- Start ---

# Calculating variables
$CurrentDate = Get-Date
$ScriptFolder = $MyInvocation.MyCommand.Path.SubString(0,($MyInvocation.MyCommand.Path.Length `
  - $MyInvocation.MyCommand.Name.Length))
$LogFile = $ScriptFolder + 'Logs\' + (Get-Date -format yyyy_MM_dd) + "_" `
  + $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".log"
  
# Log-file creation
If (-not(Test-Path ($ScriptFolder + 'Logs') -PathType Container )) {
  New-Item -ItemType Directory -Path ($ScriptFolder + 'Logs')
}
Out-File -FilePath $LogFile

Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " started")

Write-ScriptLog -LogFile $LogFile -Message ("============================== Input data ==============================")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Account ID = $AWSAccountID")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Region = $AWSRegion")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Vault Name = $AWSVaultName")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Job Id File Name = $AWSJobIdFileName")

Write-ScriptLog -LogFile $LogFile -Message ("============================== Processing ==============================")

# Input file name
$AWSJobIdFilePath = $ScriptFolder + $AWSJobIdFileName
$ProcessedArchives = $Nothing
$NotProcessedArchives = $Nothing

If (Test-Path($AWSJobIdFilePath)) {

  Write-ScriptLog -LogFile $LogFile -Message ("Reading $AWSJobOutputFileName file")
  $AWSGlacerJobId = Get-Content $AWSJobIdFilePath
  Write-ScriptLog -LogFile $LogFile -Message ("Inventory retrieval job id is $AWSGlacerJobId")

  Write-ScriptLog -LogFile $LogFile -Message ("Connecting to AWS")
  $AWSEndpoint = [Amazon.RegionEndpoint]::GetBySystemName($AWSRegion)
  # Set inventory job for a AWS Glacier vault
  $AWSGlacierClient = [Amazon.Glacier.AmazonGlacierClient]::New($AWSProfileAccessKey, $AWSProfileSecretKey, $AWSEndpoint)
  Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is opened")

  Write-ScriptLog -LogFile $LogFile -Message ("Requesting job output")
  $AWSGlacierJobOutputRequest  = [Amazon.Glacier.Model.GetJobOutputRequest]::new()
  $AWSGlacierJobOutputRequest.AccountId = $AWSAccountID
  $AWSGlacierJobOutputRequest.VaultName = $AWSVaultName
  $AWSGlacierJobOutputRequest.JobId = $AWSGlacerJobId
  Try {
    $AWSGlacierOutputResult = $AWSGlacierClient.GetJobOutput($AWSGlacierJobOutputRequest)
    Write-ScriptLog -LogFile $LogFile -Message ("Job output is received with status " + $AWSGlacierOutputResult.Status)
  }
  Catch {
    Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
  }

  If ($AWSGlacierOutputResult -ne $Nothing) {
      Write-ScriptLog -LogFile $LogFile -Message ("Parsing job output")
      [Byte[]]$buffer = New-Object System.Byte[] 4096
      $EncodedText = New-Object -TypeName System.Text.ASCIIEncoding
      $AWSGlacierOutputResultContent = $Nothing
      While(($i = $AWSGlacierOutputResult.Body.Read($buffer, 0, $buffer.Length)) -ne 0) {
        $AWSGlacierOutputResultContent += $EncodedText.GetString($buffer, 0, $i)
      }
      $AWSGlacierContent = $AWSGlacierOutputResultContent | ConvertFrom-Json
      Write-ScriptLog -LogFile $LogFile -Message ("Job output is parsed")
      Write-ScriptLog -LogFile $LogFile -Message ("There are " + $AWSGlacierContent.ArchiveList.Count + " archives in the vault")
      $AWSGlacierClient.Dispose()
      Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is closed")

      Write-ScriptLog -LogFile $LogFile -Message ("Updating archives statuses in the database")
      # Requesting archives from the database
      $Query = "SELECT * FROM " + $SQLTable + " WHERE [state] = 'transfered'"
      $TransferedFiles = Invoke-Query -ServerInstance $SQLServer -Database $SQLDatabase -Username $SQLUsername `
        -Password $SQLPassword -Query $Query
      Write-ScriptLog -LogFile $LogFile -Message ("There are " + $TransferedFiles.Count + " archives transfered to AWS Glacier")
      ForEach ($CurrentFile in $TransferedFiles) {
        $CurrentFileId = $CurrentFile."id"
        $CurrentFileName = $CurrentFile."archive-name"
        $CurrentFileAwsId = $CurrentFile."aws-archive-id"
        $CurrentFilePath = $ArchiveFolder + "\" + $CurrentFileName
        # Looking for current archive id in the vault
        Write-ScriptLog -LogFile $LogFile -Message ("  $CurrentFileName is waiting for successfull transrefing confirmation")
        ForEach ($AWSArchive in $AWSGlacierContent.ArchiveList) {
          If ($AWSArchive.ArchiveId -eq $CurrentFileAwsId) {
            Write-ScriptLog -LogFile $LogFile -Message ("    $CurrentFileName is found is the vault")
            If ((Get-Item $CurrentFilePath).Length -eq $AWSArchive.Size) {
              Write-ScriptLog -LogFile $LogFile -Message ("    The size of the archives at local server and at AWS are the same")
              Try {
                Remove-Item $CurrentFilePath -Force -Confirm:$False -ErrorAction Stop
                Write-ScriptLog -LogFile $LogFile -Message ("    $CurrentFilePath is deleted")
              } Catch {
                Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
              }
              Write-ScriptLog -LogFile $LogFile -Message ("    Updating the arhive status in the database")
              $SQLAWSArchiveId = $AWSTransferResult.ArchiveId
              $SQLAccessTimeStamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
              $SQLState = "archived"
              $SQLNotice = "size: " + $AWSArchive.size + " bytes, creation: " + $AWSArchive.CreationDate
              $UpdateQuery = "UPDATE " + $SQLTable + " SET [state] = '$SQLState',"
              $UpdateQuery += " [notice] = '$SQLNotice', [last-access-timestamp] = '$SQLAccessTimeStamp' WHERE [id] = $CurrentFileId"
              Invoke-NonQuery -ServerInstance $SQLServer -Database $SQLDatabase -Username $SQLUsername `
                -Password $SQLPassword -Query $UpdateQuery
              $ProcessedArchives += $CurrentFileName + "; "
            } Else {
              $FailedArchives += $CurrentFileName + "; "
            }
          }
        }
      }
      Write-ScriptLog -LogFile $LogFile -Message ("Archives statuses are updated the database")
     
      Send-MailMessage -SmtpServer $MailServer -Encoding $MailEncoding -From $MailFrom -To $MailTo -CC $MailCC `
        -Subject $MailSubject -BodyAsHtml "Successfully processed archives: $ProcessedArchives<br>" + `
          "Failed archives: $FailedArchives<br />Please review $LogFile on $env:computername for details."

      Write-ScriptLog -LogFile $LogFile -Message ("Deleting $AWSJobIdFileName file")
      Try {
        Remove-Item $AWSJobIdFilePath -Force -Confirm:$False -ErrorAction Stop
      Write-ScriptLog -LogFile $LogFile -Message ("$AWSJobIdFileName file is deleted")
      } Catch {
        Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
      }

  } Else {
    Write-ScriptLog -LogFile $LogFile -Message ("Job output is empty")

    Send-MailMessage -SmtpServer $MailServer -Encoding $MailEncoding -From $MailFrom -To $MailTo -CC $MailCC `
      -Subject $MailSubject -BodyAsHtml "AWS job output is empty<br />Please review $LogFile on $env:computername for details."

    If (((Get-Date) - (get-item $AWSJobIdFilePath).CreationTime).Days -ge 2) {
      Write-ScriptLog -LogFile $LogFile -Message ("$AWSJobIdFileName file is outdated")
      Write-ScriptLog -LogFile $LogFile -Message ("Deleting $AWSJobIdFileName file")
      Try {
        Remove-Item $AWSJobIdFilePath -Force -Confirm:$False -ErrorAction Stop
      Write-ScriptLog -LogFile $LogFile -Message ("$AWSJobIdFileName file is deleted")
      } Catch {
        Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
      }
    }
  }
}
Else {
  Write-ScriptLog -LogFile $LogFile -Message ("The processing is stopped")
  Write-ScriptLog -LogFile $LogFile -Message ("Use Process-AWSGlacierInventory.ps1 script to get information about AWS archives")
}

# --- End ---
Write-ScriptLog -LogFile $LogFile -Message ("========================================================================")
Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " stopped")

Входные данные скрипта:

  • ArchiveFolder - путь, куда скрипт сохраняет ZIP-архивы
  • MailServer - почтовый сервер компании для отправки уведомлений о работе скрипта
  • MailEncoding - кодировка писем
  • MailFrom - от чьего имени отправляются письма
  • MailSubject - тема писем
  • MailTO - кому отправлять письма
  • MailCC - кому отправлять копии писем
  • AWSAccountID - ID учетной записи Amazon Web Services
  • AWSRegion - регион, где находится коллекция архивов
  • AWSVaultName - имя коллекции архивов
  • AWSProfileAccessKey - учетная запись AWS с правом доступа к коллекции архивов
  • AWSProfileSecretKey - ключ к учетной записи AWS
  • SQLServer - SQL сервер компании
  • SQLDatabase - имя базы данных
  • SQLTable - имя таблицы
  • SQLUsername - имя пользователя с правом вносить изменения в SQL базу данных
  • SQLPassword - пароль пользователя SQL
  • AWSJobIdFileName - имя файла, в котором нужно сохранить номер задания (файл сохраняется в той же папке, где лежит скрипт)

Результат работы скрипта:

  • Завершенная процедура архивирования файла в облаке AWS Glacier.
  • Лог-файл работы скрипта.
Продолжение в заметке "Servers - Архивирование в AWS Glacier (часть 5)".
 

24.07.2017
Servers - Архивирование в AWS Glacier (часть 3)

В этой заметке описывается скрипт для установки задания по инвентаризации облачного архива AWS Glacier для того, чтобы удостовериться, что файл, скопированный на предыдущем этапе, успешно размещен в облаке.

Логика скрипта:

  • Подключиться к AWS Glacier.
  • Создать задание на инвентаризацию коллекции архивов.
  • Создать файл с номером задания, чтобы через 4-12 часов считать результаты инвентаризации, основываясь на записанном в файле номере задания.

Код скрипта:

# AWS variables
$AWSAccountID = 'AWS_Account_ID'
$AWSRegion = 'AWS_region'
$AWSVaultName = 'AWS_Vault_Name'
$AWSProfileAccessKey = "AWS_Access_Key"
$AWSProfileSecretKey = "AWS_Secret_Key"
$AWSJobIdFileName = "AWSGlacier-InventoryJobId.txt"

# Registering AWS libraries
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Core.dll"
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Glacier.dll"

Function Write-ScriptLog {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Message,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$LogFile
  )
  Process {
    $LogMessage = Get-Date -uformat "%d.%m.%Y %H:%M:%S"
    $LogMessage += "`t"
    $LogMessage += $Message
    $LogMessage | Out-File -FilePath $LogFile -Append
  }
}#End Function

# --- Start ---

# Calculating variables
$CurrentDate = Get-Date
$ScriptFolder = $MyInvocation.MyCommand.Path.SubString(0,($MyInvocation.MyCommand.Path.Length - `
  $MyInvocation.MyCommand.Name.Length))
$LogFile = $ScriptFolder + 'Logs\' + (Get-Date -format yyyy_MM_dd) + "_" + `
  $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".log"

# Log-file creation
If (-not(Test-Path ($ScriptFolder + 'Logs') -PathType Container )) {
  New-Item -ItemType Directory -Path ($ScriptFolder + 'Logs')
}
Out-File -FilePath $LogFile

Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " started")

Write-ScriptLog -LogFile $LogFile -Message ("============================== Input data ==============================")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Account ID = $AWSAccountID")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Region = $AWSRegion")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Vault Name = $AWSVaultName")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Job Id File Name = $AWSJobIdFileName")

Write-ScriptLog -LogFile $LogFile -Message ("============================== Processing ==============================")

# Output file name
$AWSJobIdFilePath = $ScriptFolder + $AWSJobIdFileName

If (-not (Test-Path($AWSJobIdFilePath))) {
  $AWSEndpoint = [Amazon.RegionEndpoint]::GetBySystemName($AWSRegion)
  # Set inventory job for a AWS Glacier vault
  $AWSGlacierClient = [Amazon.Glacier.AmazonGlacierClient]::New($AWSProfileAccessKey, $AWSProfileSecretKey, $AWSEndpoint)
  Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is opened")
  $AWSGlacierJobParameters  = [Amazon.Glacier.Model.JobParameters]::new()
  $AWSGlacierJobParameters.Type = "inventory-retrieval"
  $AWSGlacierJobParameters.Description = "Retrieve inventory of a vault"
  $AWSGlacierJobRequest  = [Amazon.Glacier.Model.InitiateJobRequest]::new($AWSAccountID, $AWSVaultName, $AWSGlacierJobParameters)
  Try {
    $AWSGlacerJobId = $AWSGlacierClient.InitiateJob($AWSGlacierJobRequest).JobId
    Write-ScriptLog -LogFile $LogFile -Message ("Inventory retrieval job is set with Id $AWSGlacerJobId")
  }
  Catch {
    Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
  }
  # Saving the result to the output file
  If ($AWSGlacerJobId -ne $Nothing) {
    $AWSGlacerJobId | Out-File -FilePath $AWSJobIdFilePath
    $AWSGlacierClient.Dispose()
  } Else {
    Write-ScriptLog -LogFile $LogFile -Message ("Job is not set")
  }
  Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is closed")
}
Else {
  Write-ScriptLog -LogFile $LogFile -Message ("The processing is stopped")
  Write-ScriptLog -LogFile $LogFile -Message ("Use Process-AWSGlacierChecking.ps1 script to process existing Job Id")
}

# --- End ---
Write-ScriptLog -LogFile $LogFile -Message ("========================================================================")
Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " stopped")

Входные данные скрипта:

  • AWSAccountID - ID учетной записи Amazon Web Services
  • AWSRegion - регион, где находится коллекция архивов
  • AWSVaultName - имя коллекции архивов
  • AWSProfileAccessKey - учетная запись AWS с правом доступа к коллекции архивов
  • AWSProfileSecretKey - ключ к учетной записи AWS
  • AWSJobIdFileName - имя файла, в котором нужно сохранить номер задания (файл сохраняется в той же папке, где лежит скрипт)

Результат работы скрипта:

  • Установленное задание на инвентаризацию коллекции архивов в AWS Glacier.
  • Файл с номером задания по инвентаризации.
  • Лог-файл работы скрипта.
Продолжение в заметке "Servers - Архивирование в AWS Glacier (часть 4)".
 

21.07.2017
Servers - Архивирование в AWS Glacier (часть 2)

В этой заметке описывается скрипт для создания архива из набора файлов на сервере компании и копирования этого архива в AWS Glacier.

Логика скрипта:

  1. Проверка наличия папки-источника для архивирования.
  2. Упаковка папки в ZIP-архив.
  3. Проверка содержимого ZIP-архива и составление списка файлов в архиве.
  4. Удаление файлов из папки-источника по списку файлов из ZIP-архива.
  5. Удаление оригинальной папки, если ее размер равен нулю (если удалены все файлы на предыдущем этапе).
  6. Регистрация архива и его содержимого в базе данных компании, установка состояния архива как ready to transfer.
  7. Проверка наличия файлов для копирования в AWS Glacier (поиск в базе данных архивов с состоянием ready to transfer).
  8. Проверка размера архива (размер должен быть менее 4 ГБ).
  9. Копирование архива в AWS Glacier.
  10. Регистрация состояния архива в базе данных как transfered.

Код скрипта:

# Folders for archiving
$SourceFolder = "C:\1_Archive"
# Folder for saving archives
$ArchiveFolder = "C:\2_Transfer"

# Notifications
$MailServer = "mail.domain.com"
$MailEncoding = [System.Text.Encoding]::UTF8
$MailFrom = "$env:computername@domain.com"
$MailSubject = "Glacier backup"
[string[]]$MailTO = "admin@domain.com"
[string[]]$MailCC = ""

# AWS variables
$AWSAccountID = 'AWS_Account_ID'
$AWSRegion = 'AWS_region'
$AWSVaultName = 'AWS_Vault_Name'
$AWSProfileAccessKey = "AWS_Access_Key"
$AWSProfileSecretKey = "AWS_Secret_Key"

# SQL variables
$SQLServer = 'SQLServer.domain.com'
$SQLDatabase = 'AWS-Glacier'
$SQLTable = '[archive-2017]'
$SQLUsername = 'db_user'
$SQLPassword = 'db_password'

# Registering AWS libraries
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Core.dll"
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Glacier.dll"
Add-Type -Path "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\System.IO.Compression.FileSystem.dll"

Function Write-ScriptLog {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Message,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$LogFile
)
Process {
$LogMessage = Get-Date -uformat "%d.%m.%Y %H:%M:%S"
$LogMessage += "`t"
$LogMessage += $Message
$LogMessage | Out-File -FilePath $LogFile -Append
}
}#End Function

Function Get-FolderSize {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Path
)
Begin {
$oFSO = New-Object -comobject Scripting.FileSystemObject
}
Process{
$oFolder = $oFSO.GetFolder($Path)
Return ($oFolder.Size)
}
} # End Function

Function Invoke-Query {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$ServerInstance,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Database,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Username,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Password,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Query
)
Process {
$ConnectionString = `
"Server=$ServerInstance;uid=$Username;pwd=$Password;Database=$Database;Integrated Security=False;"
$Connection = New-Object System.Data.SqlClient.SqlConnection
$Connection.ConnectionString = $ConnectionString
$Connection.Open()
$Command = New-Object System.Data.SQLClient.SQLCommand
$Command.Connection = $Connection
$Command.CommandText = $Query
$Result = $Command.ExecuteReader()
$Datatable = New-Object “System.Data.DataTable”
$Datatable.Load($Result)
$Connection.Close()
Return $Datatable
}
}#End Function

Function Invoke-NonQuery {
Param(
[CmdletBinding()]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$ServerInstance,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Database,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Username,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Password,
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[String]$Query
)
Process {
$ConnectionString = `
"Server=$ServerInstance;uid=$Username;pwd=$Password;Database=$Database;Integrated Security=False;"
$Connection = New-Object System.Data.SqlClient.SqlConnection
$Connection.ConnectionString = $ConnectionString
$Connection.Open()
$Command = New-Object System.Data.SQLClient.SQLCommand
$Command.Connection = $Connection
$Command.CommandText = $Query
$Result = $Command.ExecuteNonQuery()
$Connection.Close()
}
}#End Function

# --- Start ---

# Calculating variables
$CurrentDate = Get-Date
$ScriptFolder = $MyInvocation.MyCommand.Path.SubString(0,($MyInvocation.MyCommand.Path.Length `
- $MyInvocation.MyCommand.Name.Length))
$LogFile = $ScriptFolder + 'Logs\' + (Get-Date -format yyyy_MM_dd) + "_" `
+ $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".log"

# Log-file creation
If (-not(Test-Path ($ScriptFolder + 'Logs') -PathType Container )) {
New-Item -ItemType Directory -Path ($ScriptFolder + 'Logs')
}
Out-File -FilePath $LogFile


Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " started")

Write-ScriptLog -LogFile $LogFile -Message ("======================== Input data ========================")
Write-ScriptLog -LogFile $LogFile -Message ("Source folders = $SourceFolder")
Write-ScriptLog -LogFile $LogFile -Message ("Archive folder = $ArchiveFolder")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Account ID = $AWSAccountID")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Region = $AWSRegion")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Vault Name = $AWSVaultName")
Write-ScriptLog -LogFile $LogFile -Message ("SQL Server = $SQLServer")
Write-ScriptLog -LogFile $LogFile -Message ("SQL Database = $SQLDatabase")
Write-ScriptLog -LogFile $LogFile -Message ("SQL Username = $SQLUsername")

Write-ScriptLog -LogFile $LogFile -Message ("======================== Processing ========================")


# Compressing files
Write-ScriptLog -LogFile $LogFile -Message ("------------------------ Archiving -------------------------")
#[Void][Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem')
$ArchiveCompression = [System.IO.Compression.CompressionLevel]::Fastest
$TextEncoding = [system.Text.Encoding]::UTF8
Write-ScriptLog -LogFile $LogFile -Message ("Archiving is started")
$SourceSubFolders= Get-ChildItem $SourceFolder -Force -Directory
Write-ScriptLog -LogFile $LogFile -Message ("There are " + $SourceSubFolders.Count `
+ " folders for archiving")
ForEach ($Folder in $SourceSubFolders) {
$FolderPath = $Folder.FullName
Write-ScriptLog -LogFile $LogFile -Message ("Source folder is $FolderPath")
$FolderSize = Get-FolderSize -Path $FolderPath
Write-ScriptLog -LogFile $LogFile -Message (" Source folder size is " `
+ [math]::ceiling($FolderSize / 1mb) + " MB")
If ($FolderSize -gt 0) {
$ArchiveName = $Folder.Name.Replace("\", "_").Replace(" ","") + ".zip"
$ArchivePath = $ArchiveFolder + "\" + $ArchiveName
Write-ScriptLog -LogFile $LogFile -Message (" Archive file is $ArchiveName")
Write-ScriptLog -LogFile $LogFile -Message (" Archiving started")
# Making ZIP-file
If (Test-Path ($ArchivePath) -PathType Leaf ) { Remove-Item $ArchivePath -Force -ErrorAction Stop }
[System.IO.Compression.ZipFile]::CreateFromDirectory($FolderPath, $ArchivePath, `
$ArchiveCompression, $False)
Write-ScriptLog -LogFile $LogFile -Message (" Archiving finished")
Write-ScriptLog -LogFile $LogFile -Message (" Archive size is " `
+ [math]::ceiling((Get-Item $ArchivePath).Length / 1mb) + " MB")
# Testing ZIP-file an deleting source files
Write-ScriptLog -LogFile $LogFile -Message (" Testing the archive and removing source files")
$ArchiveFiles = [IO.Compression.ZipFile]::OpenRead($ArchivePath)
$ArchiveContent = $Nothing
ForEach ($CurrentArchiveFile in $ArchiveFiles.Entries) {
$ArchiveContent += $CurrentArchiveFile.FullName.Replace("/","\") + "`t" `
+ $CurrentArchiveFile.Length + "`t" `
+ $CurrentArchiveFile.LastWriteTime.ToString("dd.MM.yyyy") + "`r`n"
$CurrentFilePath = ($FolderPath + '\' + $CurrentArchiveFile.FullName.Replace("/","\"))
If (Test-Path($CurrentFilePath) -PathType Leaf) {
Try {
Remove-Item $CurrentFilePath -Force -ErrorAction Stop
} Catch {
Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
Send-MailMessage -SmtpServer $MailServer -Encoding $MailEncoding -From $MailFrom -To $MailTo `
-CC $MailCC -Subject $MailSubject `
-BodyAsHtml "$_.Exception.Message<br />Please review $LogFile on $env:computername for details."
}
}
}
$ArchiveFiles.Dispose()
# Checking source folder size after archiving and deleting it
$CurrentFolderSize = Get-FolderSize -Path $Folder.FullName
If ($CurrentFolderSize -eq 0) {
Try {
Remove-Item $Folder.FullName -Recurse -Force -Confirm:$False -ErrorAction Stop
} Catch {
Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
Send-MailMessage -SmtpServer $MailServer -Encoding $MailEncoding -From $MailFrom -To $MailTo `
-CC $MailCC -Subject $MailSubject `
-BodyAsHtml "$_.Exception.Message<br />Please review $LogFile on $env:computername for details."
}
}
# Prepare values for SQL query
$SQLArchiveName = $ArchiveName.Replace("'","''")
$SQLArchiveContent = $ArchiveContent.Replace("'","''")
$SQLArchiveTimeStamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$SQLState = "ready to transfer"
$SQLNotice = $ArchivePath
# Registering the arhive in the database
Write-ScriptLog -LogFile $LogFile -Message (" Registering the arhive in the database")
$InsertQuery = "INSERT INTO " + $SQLTable `
+ " ([archive-name],[aws-account-id],[aws-vault-name],[aws-archive-id],[archive-content]," `
+ "[creation-timestamp],[last-access-timestamp],[state],[notice])"
$InsertQuery += " VALUES(N'{0}', null, null, null, N'{1}', '{2}', null, '{3}', N'{4}')" -f `
$SQLArchiveName, $SQLArchiveContent, $SQLArchiveTimeStamp, $SQLState, $SQLNotice
Invoke-NonQuery -ServerInstance $SQLServer -Database $SQLDatabase `
-Username $SQLUsername -Password $SQLPassword -Query $InsertQuery
} Else {
Write-ScriptLog -LogFile $LogFile -Message (" Nothing to archive, deleting the source forlder")
Try {
Remove-Item $Folder.FullName -Recurse -Force -Confirm:$False -ErrorAction Stop
} Catch {
Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
Send-MailMessage -SmtpServer $MailServer -Encoding $MailEncoding -From $MailFrom -To $MailTo `
-CC $MailCC -Subject $MailSubject `
-BodyAsHtml "$_.Exception.Message<br />Please review $LogFile on $env:computername for details."
}
}
}
Write-ScriptLog -LogFile $LogFile -Message ("Archiving is finished")

# Transfering files
Write-ScriptLog -LogFile $LogFile -Message ("------------------------ Transfering -----------------------")
# Openning AWS connection
$AWSEndpoint = [Amazon.RegionEndpoint]::GetBySystemName($AWSRegion)
$AWSGlacierTransferManager = [Amazon.Glacier.Transfer.ArchiveTransferManager]::New($AWSProfileAccessKey, `
$AWSProfileSecretKey, $AWSEndpoint)
Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is opened")
# Requesting archives from the database
$Query = "SELECT * FROM " + $SQLTable + " WHERE [state] = 'ready to transfer'"
$TransferFiles = Invoke-Query -ServerInstance $SQLServer -Database $SQLDatabase `
-Username $SQLUsername -Password $SQLPassword -Query $Query
Write-ScriptLog -LogFile $LogFile -Message ("There are " + $TransferFiles.Count `
+ " archives for transfering to AWS Glacier")
ForEach ($CurrentFile in $TransferFiles) {
$CurrentFileId = $CurrentFile."id"
$CurrentFileName = $CurrentFile."archive-name"
$CurrentFilePath = $CurrentFile."notice"
If (Test-Path($CurrentFilePath) -PathType Leaf) {
Write-ScriptLog -LogFile $LogFile `
-Message ("$CurrentFilePath archive is ready to be transfered to AWS Glacier")
$CurrentFileSize = (Get-Item $CurrentFilePath).Length
If ($CurrentFileSize -lt 4294967296) {
Write-ScriptLog -LogFile $LogFile -Message (" The archive size is OK for transferring in one shot")
# Transferring the file to AWS Glacier
Write-ScriptLog -LogFile $LogFile -Message (" Transfering the archive to AWS Glacier")
$AWSTransferResult = $AWSGlacierTransferManager.Upload($AWSVaultName, $ArchiveDesc, $CurrentFilePath)
Write-ScriptLog -LogFile $LogFile -Message (" The archive is transfered to AWS Glacier and has Id " `
+ $AWSTransferResult.ArchiveId)
# Registering the arhive in the database
Write-ScriptLog -LogFile $LogFile -Message (" Registering the arhive in the database")
$SQLAWSAccountId = $AWSAccountID
$SQLAWSVaultName = $AWSVaultName
$SQLAWSArchiveId = $AWSTransferResult.ArchiveId
$SQLAccessTimeStamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$SQLState = "transfered"
$UpdateQuery = "UPDATE " + $SQLTable + " SET [aws-account-id] = '$SQLAWSAccountId', [aws-vault-name] " `
+ "= '$AWSVaultName', [aws-archive-id] = '$SQLAWSArchiveId',"
$UpdateQuery += " [state] = '$SQLState', [last-access-timestamp] = '$SQLAccessTimeStamp' " `
+ "WHERE [id] = $CurrentFileId"
Invoke-NonQuery -ServerInstance $SQLServer -Database $SQLDatabase -Username $SQLUsername `
-Password $SQLPassword -Query $UpdateQuery
Write-ScriptLog -LogFile $LogFile `
-Message (" The arhive is registered in the database with ID $CurrentFileId")
} Else {
Write-ScriptLog -LogFile $LogFile `
-Message (" The archive size is too big for transferring in one shot")
Write-ScriptLog -LogFile $LogFile -Message (" Use multipart upload method")

Send-MailMessage -SmtpServer $MailServer -Encoding $MailEncoding -From $MailFrom -To $MailTo `
-CC $MailCC -Subject $MailSubject `
-BodyAsHtml "$CurrentFilePath archive size is too big for transferring in one shot<br />" `
+ "Please review $LogFile on $env:computername for details."

$SQLAccessTimeStamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$SQLState = "too big size"
$UpdateQuery = "UPDATE " + $SQLTable + " SET [state] = '$SQLState', [last-access-timestamp] = " `
+ "'$SQLAccessTimeStamp' WHERE [id] = $CurrentFileId"
Invoke-NonQuery -ServerInstance $SQLServer -Database $SQLDatabase -Username $SQLUsername `
-Password $SQLPassword -Query $UpdateQuery
}
}
Else {
Write-ScriptLog -LogFile $LogFile -Message ("$CurrentFilePath archive is not found")
Send-MailMessage -SmtpServer $MailServer -Encoding $MailEncoding -From $MailFrom -To $MailTo `
-CC $MailCC -Subject $MailSubject `
-BodyAsHtml "$CurrentFilePath archive is not found<br />" `
+ "Please review $LogFile on $env:computername for details."
$SQLAccessTimeStamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$SQLState = "archive not found"
$UpdateQuery = "UPDATE " + $SQLTable + " SET [state] = '$SQLState', [last-access-timestamp] = " `
+ "'$SQLAccessTimeStamp' WHERE [id] = $CurrentFileId"
Invoke-NonQuery -ServerInstance $SQLServer -Database $SQLDatabase `
-Username $SQLUsername -Password $SQLPassword -Query $UpdateQuery
}
}
$AWSGlacierTransferManager.Dispose()
Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is closed")

# --- End ---
Write-ScriptLog -LogFile $LogFile -Message ("============================================================")
Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " stopped")

Входные данные скрипта:

  • SourceFolder - пусть, где хранятся папки-источники
  • ArchiveFolder - путь, куда скрипт сохраняет ZIP-архивы
  • MailServer - почтовый сервер компании для отправки уведомлений о работе скрипта
  • MailEncoding - кодировка писем
  • MailFrom - от чьего имени отправляются письма
  • MailSubject - тема писем
  • MailTO - кому отправлять письма
  • MailCC - кому отправлять копии писем
  • AWSAccountID - ID учетной записи Amazon Web Services
  • AWSRegion - регион, где находится коллекция архивов
  • AWSVaultName - имя коллекции архивов
  • AWSProfileAccessKey - учетная запись AWS с правом доступа к коллекции архивов
  • AWSProfileSecretKey - ключ к учетной записи AWS
  • SQLServer - SQL сервер компании
  • SQLDatabase - имя базы данных
  • SQLTable - имя таблицы
  • SQLUsername - имя пользователя с правом вносить изменения в SQL базу данных
  • SQLPassword - пароль пользователя SQL

Результат работы скрипта:

  • Упакованная папка-исходник, которая хранится и на сервере компании, и в облаке AWS Glacier.
  • Запись в базе данных компании об упакованной папке, её содержимом и состоянии в облаке.
  • Лог-файл работы скрипта.
Продолжение в заметке "Servers - Архивирование в AWS Glacier (часть 3)".
 

19.07.2017
Servers - Архивирование в AWS Glacier (часть 1)

Amazon Web Services Glacier - это облачный архив с низкой стоимостью хранения - это место для сохранения файлов с длительным сроком хранения и редким использованием. Другими словами - это хранилище для того, что жалко удалить, что может понадобиться через несколько лет. Главным преимуществом этого сервиса является цена - около 5 центов за 1 гигабайт в год.

AWS Glacier не очень удобен в использовании с точки зрения конечного пользователя, так как работа с архивами растягивается на несколько часов и даже суток, к тому же интерфейс у сервиса не позволяет управлять файлами - большинство операций нужно делать из командной строки. В этой серии заметок я предлагаю решение, которое позволит компании автоматически архивировать файлы для хранения в AWS Glacier. Моё решение основано на скриптах PowerShell, библиотеках AWS Tools For Windows, базе данных MS SQL, расписании Windows Task Scheduler и веб-сервере IIS.

Логика работы проекта:

  1. Подготовка файлов к перемещению в облачный архив - сжатие в ZIP архив.
  2. Регистрация архива и его содержимого в базе данных компании.
  3. Копирование архива в облачное хранилище.
  4. Индексирование облачного хранилища.
  5. Сравнение размеров архивов на сервере компании и в AWS Glacier.
  6. Удаление оригинального архива с сервера компании.

Составные части проекта:

  1. Коллекция архивов (vault) в AWS Glacier, куда сохраняются архивы компании.
  2. Сервер Windows Server, где собираются файлы для архивирования и происходят все процессы с архивами.
  3. База данных MS SQL, где хранится информация об архивах, их содержимом, статусах архивирования и времени создании.
  4. Скрипт для создания архива из набора файлов на сервере компании.
  5. Скрипт для копирования архива в AWS Glacier (совмещен со скриптом создания архива).
  6. Скрипт для индексирования содержимого коллекции в AWS Glacier.
  7. Скрипт для сравнения размеров архивов и удалении файлов на сервере компании.
  8. Веб-сайт ASP.NET на сервере IIS для отображения базы данных об архивах.
  9. Вспомогательный скрипт для нарезки больших архивов на части и формировании команд для ручной загрузки в AWS Glacier (подробнее о скрипте в этой заметке).
Продолжение в заметке "Servers - Архивирование в AWS Glacier (часть 2)".
 

17.07.2017
Servers - Резервное копирование в AWS EC2 через Lambda

Для того, чтобы обеспечить автоматическое резервное сохранение серверов в Amazon Web Services EC2 я предлагаю воспользоваться скриптом Python, который запускается через сервис AWS Lambda.

Логика скрипта:

  1. Проверить все работающие серверы на наличие метки Backup со значением Yes.
  2. Создать снимок со всех дисков сервера, где указана метка резервного копирования.
  3. Проверить старые автоматические снимки дисков (имя снимка начинается с autosnapshot) на предмет устаревания (более 2 дней).
  4. Удалить старые снимки дисков, найденные на предыдущем шаге.

Код скрипта:

import boto3
import json, datetime
from datetime import tzinfo, timedelta, datetime


print('Loading function')

def lambda_handler(event, context):
    regions = [context.invoked_function_arn.split(':')[3]]
    if 'regions' in event:
        regions = event['regions']

    retention_days = 2
    if 'retention_days' in event:
        retention_days = event['retention_days']

    print("AWS snapshot backups stated at %s...\n" % datetime.now())
    
    for region in regions:
        print("Region: %s" % region)
        create_region_snapshots(region, retention_days)
        
    print("\nAWS snapshot backups completed at %s\n" % datetime.now())

# create snapshot for region
def create_region_snapshots(region, retention_days):
    ec2 = boto3.resource('ec2', region_name=region)
    instances = ec2.instances.filter(
        Filters=[
            {'Name': 'instance-state-name', 'Values': ['running']},
            {'Name': 'tag:Backup', 'Values':['yes','Yes']}
        ])
    for i in instances:
        instance_name = filter(lambda tag: tag['Key'] == 'Name', i.tags)[0]['Value']
        print("\t%s - %s" % (instance_name, i.id))
        volumes = ec2.volumes.filter(Filters=[{'Name': 'attachment.instance-id', \
          'Values': [i.id]}])
        snapshot_volumes(instance_name, retention_days, volumes)

# create and prune snapshots for volume
def snapshot_volumes(instance_name, retention_days, volumes):
    for v in volumes:
        print("\t\tVolume found: \t%s" % v.volume_id)
        create_volume_snapshot(instance_name, v)
        prune_volume_snapshots(retention_days, v)

# create snapshot for volume
def create_volume_snapshot(instance_name, volume):
    snapname = '%s-%s' % (instance_name, 
        datetime.now().strftime("%Y.%m.%d") )
    description = 'autosnapshot %s %s %s' % (instance_name, volume.volume_id,
        datetime.now().strftime("%Y.%m.%d %H:%M:%S") )
    snapshot = volume.create_snapshot(Description=description)
    if snapshot:
        snapshot.create_tags(Tags=[{'Key': 'Name', 'Value': snapname}])
        print("\t\tSnapshot created with description [%s]" % description)

# find and delete snapshots older than retention_days
def prune_volume_snapshots(retention_days, volume):
    for s in volume.snapshots.all():
        now = datetime.now(s.start_time.tzinfo)
        old_snapshot = ( now - s.start_time ) > timedelta(days=retention_days)
        if not old_snapshot or not s.description.startswith('autosnapshot '): continue
        print("\t\tDeleting snapshot [%s - %s] created [%s]" % ( s.snapshot_id, \
          s.description, str( s.start_time )))
        s.delete()

Настройка Lambda функции:

  1. Перейти в AWS Lambda через Services --> Lambda.
  2. Создать новую функцию через Create a Lambda Function.
  3. Выбрать пустую функцию Blank function.
  4. Выбрать триггер CloudWatch Events и создать новое правило для событий:
    Имя: cwr-BackupService.
    Описание: Backup schedule for Lambda.
    Тип: Schedule expression.
    Расписание: rate(1 day).
    Включено: Yes.
  5. Создать саму функцию:
    Имя: lam-BackupService.
    Описание: Backup service for EC2.
    Обработчик: Python 2.7.
    Тип кода: Edit code inline.
    Код: вставить код скрипта.
    Обработчик: lambda_function.lambda_handler.
    Роль: Create custom role (после создания роли будет Choose existing role).
    Имя роли: LAM-BackupService (откроется новое окно, где надо ввести имя роли и нажать Allow).
  6. В дополнительных настройках функции установить:
    Память: 128 MB.
    Время выполнения: 5 minutes (чем больше серверов и их размер, тем дольше будет выполняться функция).
    VPC: no VPC.
  7. Проверить все параметры и создать функцию.

Использование функции в EC2:

  1. Перейти в AWS EC2 через Services --> EC2.
  2. Выбрать любой работающий сервер.
  3. На вкладке Tags нажать Add/Edit Tags.
  4. Создать метку Backup со значением Yes.
  5. Вернуться в AWS Lambda через Services --> Lambda.
  6. Выбрать функцию lam-BackupService.
  7. Нажать кнопку Test - функция запустится, а под ней будет отображен лог работы.
    START RequestId: 0f1ec135-6ac3-11e7-a2f9-79390a179184 Version: $LATEST
    AWS snapshot backups stated at 2017-07-17 07:39:26.504412...
    
    Region: eu-west-1
    	MyTestServer - i-09c37a1234565c80b
    		Volume found: 	vol-09b7f123456f1bb2b
    		Snapshot created with description [autosnapshot MyTestServer vol-09b7f123456f1bb2b ...]
    
    AWS snapshot backups completed at 2017-07-17 07:39:31.038931
    
    END RequestId: 0f1ec135-6ac3-11e7-a2f9-79390a179184
    REPORT RequestId: 0f1ec135-6ac3-11e7-a2f9-79390a179184	Duration: 4534.80 ms	Billed Duration: 4600 ms
  8. Вернуться в AWS EC2 через Services --> EC2.
  9. Перейти на вкладку снимков томов Snapshots - здесь можно найти только что сделанный снимок с диска сервера.
  10. Перейти в AWS CloudWatch через Services --> CloudWatch.
  11. Перейти на вкладку журналов Logs и выбрать журнал /aws/lambda/lam-BackupService.
  12. Выбрав поток вывода функции от нужной даты, можно посмотреть ход выполнения операции резервного копирования за любой день.

Функция резервного копирования готова к работе.

Примечание. Длительность хранения снимков томов можно отрегулировать параметром retention_days в теле функции или указать этот параметр во входящих параметрах к функции в триггере cwr-BackupService.
 

13.07.2017
Powershell - Нарезка файлов для AWS Glacier

Сервис архивирования Amazon Web Services Glacier имеет ограничения на загрузку файлов по размеру в 4 ГБ. Чтобы обойти эту проблему и иметь возможность загружать файлы большого объема Amazon реализовал метод загрузки архивов по частям - команды initiate-multipart-upload, upload-multipart-part, complete-multipart-upload и т.п.

Я разработал PowerShell скрипт для нарезки файлов и формирования команд для инициализации, загрузки и закрытия архива в AWS Glacier.

Для работы скрипта необходимо установить на сервер, где выполняется нарезка, две программы AWS Command Line Interface и AWS Tools For Windows. Создать два файла Split-File.ps1 и Split-File.cmd с содержимым, показанным ниже.

Код скрипта Split-File.ps1:

Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Core.dll"
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Glacier.dll"

# AWS variables
$AWSAccountID = YOUR_AWS_ID
$AWSRegion = 'YOUR_AWS_REGION'
$AWSVaultName = 'YOUR_AWS_GLACIER_VAILT_NAME'
$AWSProfileAccessKey = "YOUR_AWS_SERVICE_ACCOUNT_ACCESS_KEY"
$AWSProfileSecretKey = "YOUR_AWS_SERVICE_ACCOUNT_SECRET_KEY"

if($args[0] -eq $null) {
  Write-Host "Error: No input parameter"
  Break
} else {
  [String] $InputFile = $args[0];
} 

[Int32] $PartSize = 1073741824 # 1 GB
[Int32] $PartNumber = 1
[String[]] $PartChecksumList = @()

# ------- Split-File -------

$InputStream = [System.IO.File]::OpenRead($InputFile)

$Piece = New-Object byte[] $PartSize
While ($BytesRead = $InputStream.Read($Piece, 0, $PartSize)) {
  $OutputFile = $InputFile + ".p" + $PartNumber.ToString("00")
  $OutputStream = [System.IO.File]::OpenWrite($OutputFile)
  $OutputStream.Write($Piece, 0, $BytesRead)
  $OutputStream.Close()
  $OutputStream = [System.IO.File]::OpenRead($OutputFile)
  $CurrentChecksum = [Amazon.Glacier.TreeHashGenerator]::CalculateTreeHash($OutputStream)
  $OutputStream.Close()
  $PartChecksumList += $CurrentChecksum
  $PartNumber += 1
}
$InputStream.Close()
$TreeHash = [Amazon.Glacier.TreeHashGenerator]::CalculateTreeHash([String[]]$PartChecksumList)
$OutputTreeHashFile = $InputFile + ".hash"
$TreeHash | Out-File $OutputTreeHashFile


# ------- Generate multiupload command -------

$FileDirectory = (Get-Item -Path $InputFile).DirectoryName
$FileName = (Get-Item -Path $InputFile).Name
$FileSize = (Get-Item -Path $InputFile).Length
$FileParts = Get-ChildItem $FileDirectory -Filter "$FileName.p*" | Sort-Object -Property "Name"

$MultiuploadCommandFile = $InputFile + "_Command1.txt"
Out-File -FilePath $MultiuploadCommandFile
$CommandLine = "aws glacier initiate-multipart-upload --account-id $AWSAccountID"
$CommandLine += " --vault-name $AWSVaultName --part-size $PartSize"
$CommandLine += " --archive-description ""$FileName"""
$CommandLine | Out-File -FilePath $MultiuploadCommandFile -Append

$MultiuploadCommandFile = $InputFile + "_Command2.txt"
Out-File -FilePath $MultiuploadCommandFile
"set uploadID=Put_Here_Upload_ID" | Out-File -FilePath $MultiuploadCommandFile -Append
"set uploadTreeHash=$TreeHash" | Out-File -FilePath $MultiuploadCommandFile -Append
"" | Out-File -FilePath $MultiuploadCommandFile -Append

[int] $i=1
[long] $StartByte = 0
[long] $EndByte = 0
ForEach ($FilePart in $FileParts) {
  If ($i -lt $FileParts.Count) {
    $EndByte = $StartByte + $PartSize - 1
  } Else {
    $EndByte = $FileSize - 1
  }
  $FilePartRange = """bytes $StartByte-$EndByte/*"""
  $FilePartPath = """" + $FilePart.FullName + """"
  $CommandLine = "aws glacier upload-multipart-part --account-id $AWSAccountID --vault-name $AWSVaultName"
  $CommandLine +=" --upload-id %uploadID% --range $FilePartRange --body $FilePartPath"
  $CommandLine | Out-File -FilePath $MultiuploadCommandFile -Append
  $i++
  $StartByte = $EndByte + 1
}

"" | Out-File -FilePath $MultiuploadCommandFile -Append
$CommandLine = "aws glacier complete-multipart-upload --account-id $AWSAccountID --vault-name $AWSVaultName"
$CommandLine += " --upload-id %uploadID% --archive-size $FileSize --checksum %uploadTreeHash%"
$CommandLine | Out-File -FilePath $MultiuploadCommandFile -Append

Код скрипта Split-File.cmd:

powershell.exe "%~DP0Split-File.ps1" %1

Перед запуском нарезки файла необходимо в файле Split-File.ps1 вписать данные своей учетной записи AWS с строках 5-9.

Для нарезки файла необходимо перетащить архив на скрипт Split-File.cmd, который вызовет Split-File.ps1 и подставит в качестве входного параметра путь к архиву.

Результатом работы скрипта будет:

  • Коллекция частей архива по 1 ГБ (имя_архива.p01, имя_архива.p02 и т.д).
  • Файл с контрольной суммой (имя_архива.hash).
  • Файл с командой для инициализации закачки (имя_архива_Command1.txt).
  • Файл с командами закачки чайтей архива и закрытия архива (имя_архива_Command2.txt).

Чтобы выполнить закачку разрезанного архива необходимо:

  • Открыть командную строку.
  • Вставить в командную строку содержимое файла имя_архива_Command1.txt.
  • Из ответа сервера скопировать значение uploadID.
  • В файле имя_архива_Command2.txt заменить Put_Here_Upload_ID на скопированное значение uploadID.
  • Вставить в командную строку содержимое файла имя_архива_Command2.txt - начнется поочередное выполнение команд из буфера обмена - закачка частей архива.
  • При успешной закачке всех частей в командную строку сервер AWS вернет значение archiveID, которое он присвоил новому архиву. В противном случае выдаст номер частей архива, загрузка которых не удалась.
Примечание. Учетная запись от имени которой выполняется закачка в AWS Glacier должна иметь права на чтение и закачку в указанный букет архивов. Данные этой учетной записи должны быть сконфигурированы в программе AWS Command Line Interface.
 

30.06.2017
Электроника - Контроллер Arduino Mega с ESP8266

В этой заметке я расскажу, о контроллере Arduino Mega, совмещенным с контроллером ESP8266.

История покупки данной платы следующая. Для управления макетом железной дороги по протоколу DCC++ мне понадобилась плата семейства Arduino. Сначала я выбрал микроконтроллер Arduino UNO и управлял поездами через виртуальный COM-порт компьютера (USB порт), подключенного к микроконтроллеру. Но в начале 2017 года я нашел статью "Roco z21 на базе Arduino" в которой указывалось, что макетом можно управлять по WiFi, если применить плату Arduino Mega, совмещенную с контроллером ESP8266.

Итак, плата выполнена в размерах оригинального контроллера Arduino Mega. Контроллер ATmega2560 смещен в правую сторону, а в левой стороне расположен контроллер ESP8266. Над WiFi контроллером находится USB разъем, который заменен с обычного Type-B на Micro-B, а взаимодействие этого разъема с компонентами платы осуществляется через USB-TTL конвертер CH340G и DIP переключатель из 8 контактов. Фотография платы представлена ниже.

WeMos Mega + WiFi
WeMos Mega + WiFi

Переключатель режимов USB порта может находится в режимах, описанных в таблице ниже

  1 2 3 4 5 6 7 8 RXD/TXD
USB изолирован --- --- --- --- --- --- --- --- 0
USB
<->
ATmega2560
--- --- ON ON --- --- --- --- 0
USB
<->
ESP8266 (прошивка)
--- --- --- --- ON ON ON --- 0
USB <->
ESP8266 (работа)
--- --- --- --- ON ON --- --- 0
ATmega2560
<->
ESP8266
ON ON --- --- --- --- --- --- 0
USB
<->
ATmega2560
<->
ESP8266
ON ON ON ON --- --- --- --- 3

 

Для проверки работоспособности платы необходимо протестировать работу каждого из контроллеров, а также взаимодействие контроллеров между собой. Для проверки потребуется:

  • Сама плата Mega-WiFi.
  • Компьютер с операционной системой Windows.
  • Кабель USB-microUSB.
  • Драйвер USB-TTL конвертера CH340.
  • Программная среда Arduino IDE (старые сборки можно скачать из архива версий).
  • Тестовые прошивки Arduino Mega and ESP8266.
  • Канцелярская скрепка.

 

Сначала надо установить драйвер платы, а если быть точнее, то драйвер контроллера CH340. Для этого нужно подключить плату к компьютеру с помощью USB кабеля, открыть Диспетчер устройств, найти подключенное неизвестное USB-устройство и обновить ему драйвер, указав в качестве источника драйверов папку со скачанными и распакованными драйверами CH340.

После установки драйверов устройство отобразиться в диспетчере устройств как USB-SERIAL CH340 (COMx). Надо найти это устройство и выписать номер виртуального COM-порта, к которому подключена плата, например COM4.

После того, как плата стала видна, её можно отключить от компьютера, но надо запомнить, к какому USB порту было произведено подключение и включать плату только в этот порт - если включить в другой, то номер COM-порта может измениться.

 

Теперь можно заняться установкой и настройкой программной среды Arduino IDE. Скачав нужную версию программы, например 1.8.3, и установив на свой компьютер, необходимо её настроить. Для этого надо выполнить следующее:

  1. Создать папку для своих проектов, например C:\ArduinoProjects.
  2. Распаковать в папку проектов архив Arduino Mega and ESP8266.zip.
  3. Запустить программу Arduino IDE.
  4. Открыть Настройки через Файл --> Настройки.
  5. В поле Размещение папки скетчей указать C:\ArduinoProjects.
  6. В поле Дополнительные ссылки для менеджера плат вписать
    http://arduino.esp8266.com/stable/package_esp8266com_index.json.
  7. Сохранить настройки, нажав OK.
  8. Открыть Менеджер плат через Инструменты --> Плата --> Менеджер плат.
  9. Найти в списке esp8266 by ESP8266 Comunity и установить пакет нужной версии, например 2.3.0.
  10. Закрыть программу Arduino IDE.

 

Чтобы проверить работоспособность контроллера ATmega2560 необходимо проделать следующие операции:

  1. С помощью скрепки переключить ползунки DIP-переключателя в положения, указанные в таблице ниже.
      1 2 3 4 5 6 7 8 RXD/TXD
    USB
    <->
    ATmega2560
    --- --- ON ON --- --- --- --- 0
  2. Подключить плату к компьютеру с помощью USB-кабеля.
  3. Запустить программу Arduino IDE.
  4. Выбрать плату Arduino/Genuino Mega or Mega 2560 через
    Инструменты --> Плата --> Arduino/Genuino Mega or Mega 2560.
  5. Выбрать COM-порт, который выписывали из Диспетчера устройств при установке драйвера платы, через
    Инструменты --> Порт --> COM4.
  6. Открыть тестовую программу TEST_MEGA через Файл --> Папка со скетчами --> TEST_MEGA.
  7. Загрузить прошивку в контроллер через Скетч --> Загрузка.
  8. После успешной загрузки прошивки в контроллер открыть Монитор порта через Инструменты --> Монитор порта.
  9. В Мониторе порта нужно выбрать скорость передачи в 115200 бод, после чего в окне монитора должно отобразиться меню тестовой программы.
  10. В поле ввода команд Монитора порта нужно ввести английскую букву "a" и нажать Отправить, в ответ на команду контроллер перейдет в режим мигания встроенным светодиодом в течении 20 секунд.
  11. В Монитор порта нужно отправить команду "b" - контроллер выдаст размер свободной памяти SRAM = 7873 и полный размер памяти EEPROM = 4096.
  12. В Монитор порта нужно отправить команду "c" - контроллер протестирует состояние выходов и выдаст таблицу с результатами.
    В таблице все выводы кроме 0, 1 и 13 должны быть в состоянии OK. На выводах 0, 1, 13 выявлены неисправности, так как 0 и 1 выводы заняты последовательным портом (UART), а на 13 выводе находится встроенный светодиод.
  13. Все проверки проведены - контроллер нужно отключить от USB порта компьютера.

 

Чтобы проверить работоспособность контроллера ESP8266 необходимо проделать следующие операции:

  1. С помощью скрепки переключить ползунки DIP-переключателя в положения, указанные в таблице ниже.
      1 2 3 4 5 6 7 8 RXD/TXD
    USB
    <->
    ESP8266 (прошивка)
    --- --- --- --- ON ON ON --- 0
  2. Подключить плату к компьютеру с помощью USB-кабеля.
  3. В программе Arduino IDE выбрать плату WeMos D1 R2 & mini через Инструменты --> Плата --> WeMos D1 R2 & mini.
    Примечание. Рекомендованный в документации тип платы Olimex MOD-WIFI-ESP8266(-DEV) не подходит.
  4. Проверить настройки контроллера в соответствии с рисунком.
  5. Открыть тестовую программу TEST_ESP8266 через Файл --> Папка со скетчами --> TEST_ESP8266.
  6. В строки 7 и 8 программы вместо многоточий нужно вставить имя домашней WiFi сети и её пароль.
  7. Загрузить прошивку в контроллер через Скетч --> Загрузка.
  8. После успешной загрузки отключить плату от компьютера.
  9. С помощью скрепки переключить ползунки DIP-переключателя в положения, указанные в таблице ниже.
      1 2 3 4 5 6 7 8 RXD/TXD
    USB <->
    ESP8266 (работа)
    --- --- --- --- ON ON --- --- 0
  10. Подключить плату к компьютеру с помощью USB-кабеля.
  11. С помощью Монитора порта посмотреть что выдает контроллер.
    Значения Chip Real Size (реальный размер памяти) и Chip Flash Size (размер памяти, выставленный при прошивке) должны быть одинаковыми.
    Необходимо выписать IP адрес, который получил контроллер от WiFi коммутатора - этот адрес понадобится в следующем тесте.
  12. Проверка проведена - контроллер нужно отключить от USB порта компьютера.

 

Чтобы проверить взаимодействие контроллеров ATmega2560 и ESP8266 необходимо проделать следующие операции:

  1. С помощью скрепки переключить ползунки DIP-переключателя в положения, указанные в таблице ниже.
      1 2 3 4 5 6 7 8 RXD/TXD
    USB
    <->
    ATmega2560
    <->
    ESP8266
    ON ON ON ON --- --- --- --- 3
  2. Подключить плату к компьютеру с помощью USB-кабеля.
  3. В программе Arduino IDE выбрать плату Arduino/Genuino Mega or Mega 2560 через
    Инструменты --> Плата --> Arduino/Genuino Mega or Mega 2560.
  4. Открыть тестовую программу TEST_MEGA-ESP через Файл --> Папка со скетчами --> TEST_MEGA-ESP.
  5. Загрузить прошивку в контроллер через Скетч --> Загрузка.
  6. После успешной загрузки прошивки в контроллер открыть Монитор порта через Инструменты --> Монитор порта.
  7. Контроллер Arduino Mega должен считать данные с порта обмена данных с контроллером ESP8266 и выдать их в окно Монитора порта.
  8. Открыть в браузере страницу по IP адресу, выданному в Мониторе порта.
  9. В отрытой странице поочередно нажимать на кнопки ON и OFF и следить за состоянием светодиода, встроенного на плату. Светодиод должен включаться командой ON и выключаться командой OFF.
  10. Проверка проведена - контроллер нужно отключить от USB порта компьютера.

 

Плату можно найти в интернет магазине RobotDyn, eBay или AliExpress по ключевым словам "ATmega2560 ESP8266 CH340G". Цена на плату колеблется от 10$ до 25$ в зависимости от качества исполнения и жадности продавца. Я покупал плату на AliExpress за 11$.

Еще один обзор платы можно прочитать по этой ссылке.

 

 

Вверх