enscalator 0.4.0.pre.alpha.pre.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rubocop.yml +9 -0
  4. data/.rubocop_todo.yml +59 -0
  5. data/.travis.yml +22 -0
  6. data/CODE_OF_CONDUCT.md +13 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +148 -0
  10. data/Rakefile +43 -0
  11. data/bin/console +11 -0
  12. data/bin/setup +7 -0
  13. data/enscalator.gemspec +57 -0
  14. data/exe/enscalator +13 -0
  15. data/lib/enscalator/core/cf_parameters.rb +146 -0
  16. data/lib/enscalator/core/cf_resources.rb +225 -0
  17. data/lib/enscalator/core/instance_type.rb +205 -0
  18. data/lib/enscalator/core/network_config.rb +21 -0
  19. data/lib/enscalator/core.rb +10 -0
  20. data/lib/enscalator/enapp.rb +248 -0
  21. data/lib/enscalator/helpers/dns.rb +62 -0
  22. data/lib/enscalator/helpers/stack.rb +107 -0
  23. data/lib/enscalator/helpers/sub_process.rb +72 -0
  24. data/lib/enscalator/helpers/wrappers.rb +55 -0
  25. data/lib/enscalator/helpers.rb +127 -0
  26. data/lib/enscalator/plugins/amazon_linux.rb +93 -0
  27. data/lib/enscalator/plugins/auto_scale.rb +80 -0
  28. data/lib/enscalator/plugins/core_os.rb +88 -0
  29. data/lib/enscalator/plugins/couchbase.rb +98 -0
  30. data/lib/enscalator/plugins/debian.rb +71 -0
  31. data/lib/enscalator/plugins/elastic_beanstalk.rb +74 -0
  32. data/lib/enscalator/plugins/elasticache.rb +168 -0
  33. data/lib/enscalator/plugins/elasticsearch_amazon.rb +75 -0
  34. data/lib/enscalator/plugins/elasticsearch_bitnami.rb +198 -0
  35. data/lib/enscalator/plugins/elasticsearch_opsworks.rb +225 -0
  36. data/lib/enscalator/plugins/elb.rb +139 -0
  37. data/lib/enscalator/plugins/nat_gateway.rb +71 -0
  38. data/lib/enscalator/plugins/rds.rb +141 -0
  39. data/lib/enscalator/plugins/redis.rb +38 -0
  40. data/lib/enscalator/plugins/rethink_db.rb +21 -0
  41. data/lib/enscalator/plugins/route53.rb +143 -0
  42. data/lib/enscalator/plugins/ubuntu.rb +85 -0
  43. data/lib/enscalator/plugins/user-data/elasticsearch +367 -0
  44. data/lib/enscalator/plugins/vpc_peering_connection.rb +48 -0
  45. data/lib/enscalator/plugins.rb +30 -0
  46. data/lib/enscalator/rich_template_dsl.rb +209 -0
  47. data/lib/enscalator/templates/vpc_peering.rb +112 -0
  48. data/lib/enscalator/templates.rb +20 -0
  49. data/lib/enscalator/version.rb +5 -0
  50. data/lib/enscalator/vpc.rb +11 -0
  51. data/lib/enscalator/vpc_with_nat_gateway.rb +311 -0
  52. data/lib/enscalator/vpc_with_nat_instance.rb +402 -0
  53. data/lib/enscalator.rb +103 -0
  54. metadata +427 -0
@@ -0,0 +1,71 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # VPC NAT Gateway plugin
4
+ module NATGateway
5
+ # Allocate new elastic IP in given VPC template
6
+ #
7
+ # @param [String] name eip resource name
8
+ # @param [Array<String>] depends_on list of resource names this resource depends on
9
+ # @return [Hash] result of Fn::GetAtt function
10
+ def allocate_new_eip(name, depends_on: [])
11
+ fail('Dependency on the VPC-gateway attachment must be provided') if depends_on.empty?
12
+ eip_resource_name = name
13
+ resource eip_resource_name,
14
+ DependsOn: depends_on,
15
+ Type: 'AWS::EC2::EIP',
16
+ Properties: {
17
+ Domain: 'vpc'
18
+ }
19
+
20
+ output eip_resource_name,
21
+ Description: 'Elastic IP address for NAT Gateway',
22
+ Value: ref(eip_resource_name)
23
+
24
+ get_att(eip_resource_name, 'AllocationId')
25
+ end
26
+
27
+ # Create new route rule
28
+ #
29
+ # @param [String] name route rule name
30
+ # @param [Array<String>] depends_on list of resource names this resource depends on
31
+ def add_route_rule(name, route_table_name, nat_gateway_name, dest_cidr_block, depends_on: [])
32
+ options = {
33
+ Type: 'AWS::EC2::Route'
34
+ }
35
+ options[:DependsOn] = depends_on unless depends_on.blank?
36
+ resource name,
37
+ options.merge(
38
+ Properties: {
39
+ RouteTableId: ref(route_table_name),
40
+ NatGatewayId: ref(nat_gateway_name),
41
+ DestinationCidrBlock: dest_cidr_block
42
+ })
43
+ end
44
+
45
+ # Create new NAT gateway
46
+ def nat_gateway_init(name, subnet_name, route_table_name, dest_cidr_block: '0.0.0.0/0', depends_on: [])
47
+ nat_gateway_eip_name = "#{name}EIP"
48
+ nat_gateway_eip = allocate_new_eip(nat_gateway_eip_name, depends_on: depends_on)
49
+ nat_gateway_name = name
50
+ nat_gateway_options = {
51
+ Type: 'AWS::EC2::NatGateway'
52
+ }
53
+ nat_gateway_options[:DependsOn] = depends_on unless depends_on.blank?
54
+ resource nat_gateway_name,
55
+ nat_gateway_options.merge(
56
+ Properties: {
57
+ AllocationId: nat_gateway_eip,
58
+ SubnetId: ref(subnet_name)
59
+ })
60
+ nat_route_rule_name = "#{name}Route"
61
+ add_route_rule(nat_route_rule_name, route_table_name, nat_gateway_name, dest_cidr_block, depends_on: depends_on)
62
+
63
+ output nat_gateway_name,
64
+ Description: 'NAT Gateway',
65
+ Value: ref(nat_gateway_name)
66
+
67
+ nat_gateway_name
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,141 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Amazon RDS instance
4
+ module RDS
5
+ # Create new Amazon RDS instance
6
+ #
7
+ # @param [String] db_name database name
8
+ # @param [Boolean] use_snapshot use snapshot or not
9
+ # @param [Integer] allocated_storage size of instance primary storage
10
+ # @param [String] storage_type instance storage type
11
+ # @param [String] multizone deploy as multizone or use only single availability zone
12
+ # @param [String] parameter_group RDS instance parameter group
13
+ # @param [String] instance_type instance type
14
+ # @param [Hash] properties additional properties
15
+ def rds_init(db_name,
16
+ use_snapshot: false,
17
+ allocated_storage: 5,
18
+ storage_type: 'gp2',
19
+ multizone: 'false',
20
+ engine: 'MySQL',
21
+ engine_version: '5.6',
22
+ parameter_group: 'default.mysql5.6',
23
+ instance_type: 'db.t2.small',
24
+ properties: {})
25
+
26
+ parameter_name "RDS#{db_name}"
27
+
28
+ parameter_rds_instance_type "RDS#{db_name}", type: instance_type
29
+
30
+ parameter_allocated_storage "RDS#{db_name}",
31
+ default: allocated_storage,
32
+ min: 5,
33
+ max: 1024
34
+
35
+ parameter "RDS#{db_name}Engine",
36
+ Default: engine,
37
+ Description: 'DB engine type of the DB instance',
38
+ Type: 'String'
39
+
40
+ parameter "RDS#{db_name}EngineVersion",
41
+ Default: engine_version,
42
+ Description: 'DB engine version of the DB instance',
43
+ Type: 'String'
44
+
45
+ parameter "RDS#{db_name}StorageType",
46
+ Default: storage_type,
47
+ Description: 'Storage type to be associated with the DB instance',
48
+ Type: 'String',
49
+ AllowedValues: %w( gp2 standard io1 )
50
+
51
+ parameter "RDS#{db_name}Multizone",
52
+ Default: multizone,
53
+ Description: 'Multizone deployment',
54
+ Type: 'String'
55
+
56
+ parameter "RDS#{db_name}ParameterGroup",
57
+ Default: parameter_group,
58
+ Description: 'Custom parameter group for an RDS database family',
59
+ Type: 'String'
60
+
61
+ parameter_username "RDS#{db_name}"
62
+
63
+ parameter_password "RDS#{db_name}"
64
+
65
+ resource "RDS#{db_name}SubnetGroup",
66
+ Type: 'AWS::RDS::DBSubnetGroup',
67
+ Properties: {
68
+ DBSubnetGroupDescription: 'Subnet group within VPC',
69
+ SubnetIds: ref_resource_subnets,
70
+ Tags: [
71
+ {
72
+ Key: 'Name',
73
+ Value: "RDS#{db_name}SubnetGroup"
74
+ }
75
+ ]
76
+ }
77
+
78
+ # DBName and DBSnapshotIdentifier are mutually exclusive, thus
79
+ # when snapshot_id is given DBName won't be included to resource parameters
80
+ props = properties.deep_dup
81
+ if use_snapshot
82
+ parameter "RDS#{db_name}SnapshotId",
83
+ Description: 'Identifier for the DB snapshot to restore from',
84
+ Type: 'String',
85
+ MinLength: '1',
86
+ MaxLength: '64'
87
+ props[:DBSnapshotIdentifier] = ref("RDS#{db_name}SnapshotId")
88
+ else
89
+ props[:DBName] = ref("RDS#{db_name}Name")
90
+ end
91
+
92
+ rds_instance_tags = [
93
+ {
94
+ Key: 'Name',
95
+ Value: "RDS#{db_name}Instance"
96
+ }
97
+ ]
98
+
99
+ # Set instance tags
100
+ if props.key?(:Tags) && !props[:Tags].empty?
101
+ props[:Tags].concat(rds_instance_tags)
102
+ else
103
+ props[:Tags] = rds_instance_tags
104
+ end
105
+
106
+ rds_props = {
107
+ PubliclyAccessible: 'false',
108
+ MultiAZ: ref("RDS#{db_name}Multizone"),
109
+ Engine: ref("RDS#{db_name}Engine"),
110
+ EngineVersion: ref("RDS#{db_name}EngineVersion"),
111
+ MasterUsername: ref("RDS#{db_name}Username"),
112
+ MasterUserPassword: ref("RDS#{db_name}Password"),
113
+ DBInstanceClass: ref("RDS#{db_name}InstanceType"),
114
+ VPCSecurityGroups: [ref_resource_security_group, ref_private_security_group],
115
+ DBSubnetGroupName: ref("RDS#{db_name}SubnetGroup"),
116
+ DBParameterGroupName: ref("RDS#{db_name}ParameterGroup"),
117
+ AllocatedStorage: ref("RDS#{db_name}AllocatedStorage"),
118
+ StorageType: ref("RDS#{db_name}StorageType")
119
+ }
120
+
121
+ rds_instance_resource_name = "RDS#{db_name}Instance"
122
+ resource rds_instance_resource_name,
123
+ Type: 'AWS::RDS::DBInstance',
124
+ Properties: rds_props.merge(props)
125
+
126
+ output "RDS#{db_name}EndpointAddress",
127
+ Description: "#{db_name} Endpoint Address",
128
+ Value: get_att("RDS#{db_name}Instance", 'Endpoint.Address')
129
+
130
+ rds_instance_resource_name
131
+ end
132
+
133
+ # Ensure that plugin using this template is a subclass of EnAppTemplateDSL
134
+ def self.included(klass)
135
+ if klass.superclass != Enscalator::EnAppTemplateDSL
136
+ fail("Plugin #{name.to_s.demodulize} requires template to be subclass of #{EnAppTemplateDSL}")
137
+ end
138
+ end
139
+ end # RDS
140
+ end # Plugins
141
+ end # Enscalator
@@ -0,0 +1,38 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Redis on EC2 instance
4
+ module Redis
5
+ include Enscalator::Plugins::Ubuntu
6
+
7
+ # Create new Redis instance
8
+ #
9
+ # @param [String] instance_name instance name
10
+ # @param [String] key_name instance key
11
+ # @param [String] instance_type instance type
12
+ def redis_init(instance_name,
13
+ key_name:,
14
+ instance_type: 't2.medium')
15
+
16
+ parameter "Ubuntu#{instance_name}KeyName",
17
+ Default: key_name,
18
+ Description: 'Keypair name',
19
+ Type: 'String'
20
+
21
+ ubuntu_init instance_name, instance_type: instance_type, properties: { 'UserData' => redis_user_data }
22
+ end
23
+
24
+ # Install and run Redis on EC2 instance
25
+ # @return [String] user-data
26
+ def redis_user_data
27
+ Base64.encode64(%(
28
+ #!/usr/bin/env bash
29
+ apt-get update
30
+ apt-get upgrade -y
31
+ apt-get install -y redis-server
32
+ sed -i 's/bind 127.0.0.1/bind 0.0.0.0/' /etc/redis/redis.conf
33
+ service redis-server restart
34
+ ).gsub(/^\s+/, ''))
35
+ end
36
+ end # Redis
37
+ end # Plugins
38
+ end # Enscalator
@@ -0,0 +1,21 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # RethinkDB appliance
4
+ module RethinkDB
5
+ # Mapping for Rethinkdb x64 images
6
+ def self.mapping
7
+ {
8
+ 'eu-central-1': { paravirtual: 'ami-1249740f' },
9
+ 'eu-west-1': { paravirtual: 'ami-7a40f00d' },
10
+ 'ap-northeast-1': { paravirtual: 'ami-90c3c491' },
11
+ 'us-east-1': { paravirtual: 'ami-6ea9fb06' },
12
+ 'us-west-1': { paravirtual: 'ami-7f6d7d3a' },
13
+ 'us-west-2': { paravirtual: 'ami-cf0d2cff' },
14
+ 'ap-southeast-1': { paravirtual: 'ami-aa5b6cf8' },
15
+ 'ap-southeast-2': { paravirtual: 'ami-b9325b83' },
16
+ 'sa-east-1': { paravirtual: 'ami-b38b3aae' }
17
+ }.with_indifferent_access
18
+ end
19
+ end # RethinkDB
20
+ end # Plugins
21
+ end # Enscalator
@@ -0,0 +1,143 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Create Route53 resources
4
+ module Route53
5
+ # Valid types for Route53 healthcheck
6
+ HEALTH_CHECK_TYPE = %w(HTTP HTTPS HTTP_STR_MATCH HTTPS_STR_MATCH TCP)
7
+
8
+ # Valid types for dns records
9
+ RECORD_TYPE = %w(A AAAA CNAME MX NS PTR SOA SPF SRV TXT)
10
+
11
+ # Create Route53 healthcheck for given fqdn/ip address
12
+ #
13
+ # @param [String] app_name application name
14
+ # @param [String] stack_name stack name
15
+ # @param [String] fqdn fully qualified domain name (FQDN)
16
+ # @param [String] ip_address ip address
17
+ # @param [Integer] port number
18
+ # @param [String] type healthcheck type
19
+ # @param [String] resource_path uri path healthcheck backend would query
20
+ # @param [Integer] request_interval query intervals for healthcheck backend
21
+ # @param [Integer] failure_threshold number of accumulated failures to consider endpoint not healthy
22
+ # @param [Array] tags additional tags
23
+ def create_healthcheck(app_name,
24
+ stack_name,
25
+ fqdn: nil,
26
+ ip_address: nil,
27
+ port: 80,
28
+ type: 'HTTP',
29
+ resource_path: '/',
30
+ request_interval: 30,
31
+ failure_threshold: 3,
32
+ tags: [])
33
+ unless HEALTH_CHECK_TYPE.include?(type)
34
+ fail("Route53 healthcheck type can only be one of the following: #{HEALTH_CHECK_TYPE.join(',')}")
35
+ end
36
+ fail('Route53 healthcheck requires either fqdn or ip address') unless fqdn || ip_address
37
+
38
+ properties = {
39
+ HealthCheckConfig: {
40
+ IPAddress: ip_address,
41
+ FullyQualifiedDomainName: fqdn,
42
+ Port: port,
43
+ Type: type,
44
+ ResourcePath: resource_path,
45
+ RequestInterval: request_interval,
46
+ FailureThreshold: failure_threshold
47
+ }
48
+ }
49
+
50
+ properties[:HealthCheckTags] = [
51
+ {
52
+ Key: 'Application',
53
+ Value: app_name
54
+ },
55
+ {
56
+ Key: 'Stack',
57
+ Value: stack_name
58
+ }
59
+ ]
60
+
61
+ properties[:HealthCheckTags].concat(tags) unless tags.blank?
62
+
63
+ resource "#{app_name}Healthcheck",
64
+ Type: 'AWS::Route53::HealthCheck',
65
+ Properties: properties
66
+ end
67
+
68
+ # [RESERVED] Create new hosted zone
69
+ def create_hosted_zone
70
+ fail('method "create_hosted_zone" is not implemented yet')
71
+ end
72
+
73
+ # TODO: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-recordset.html
74
+
75
+ # Create new single record set for given hosted zone
76
+ #
77
+ # @param [String] app_name application name
78
+ # @param [String] stack_name stack name
79
+ # @param [String] zone_name hosted zone name
80
+ # @param [String] record_name dns record name
81
+ # @param [Integer] ttl time to live
82
+ # @param [String] type dns record type
83
+ # @param [Hash] healthcheck reference to the healthcheck resource
84
+ # @param [Hash] alias_target alias target
85
+ # @param [Array] resource_records resources associated with record_name
86
+ def create_single_dns_record(app_name,
87
+ stack_name,
88
+ zone_name,
89
+ record_name,
90
+ ttl: 300,
91
+ type: 'A',
92
+ healthcheck: nil,
93
+ zone_id: nil,
94
+ alias_target: {},
95
+ resource_records: [])
96
+ if type && !RECORD_TYPE.include?(type)
97
+ fail("Route53 record type can only be one of the following: #{RECORD_TYPE.join(',')}")
98
+ end
99
+ if healthcheck && (!healthcheck.is_a?(Hash) || !healthcheck.include?(:Ref))
100
+ fail('healthcheck must be a valid cloudformation Ref function')
101
+ end
102
+ if alias_target && (!alias_target.is_a?(Hash))
103
+ fail('AliasTarget must be a Hash')
104
+ end
105
+
106
+ name = app_name || stack_name.titleize.remove(/\s/)
107
+ properties = {
108
+ Name: record_name,
109
+ Comment: "#{type} record for #{[app_name, 'in '].join(' ') if app_name}#{stack_name} stack",
110
+ Type: type
111
+ }
112
+
113
+ # HostedZoneId and HostedZoneName options are mutually exclusive
114
+ if zone_id && !zone_id.nil?
115
+ properties[:HostedZoneId] = zone_id
116
+ else
117
+ properties[:HostedZoneName] = zone_name
118
+ end
119
+
120
+ if !alias_target.blank?
121
+ fail('AliasTarget can be created only for A or AAAA type records') unless %w(A AAAA).include?(type)
122
+ unless alias_target.key?(:HostedZoneId) && alias_target.key?(:DNSName)
123
+ fail('AliasTarget must have HostedZoneId and DNSName properties')
124
+ end
125
+ properties[:AliasTarget] = alias_target
126
+ else
127
+ properties[:TTL] = ttl
128
+ properties[:HealthCheckId] = healthcheck if healthcheck
129
+ properties[:ResourceRecords] = resource_records.empty? ? ref("#{app_name}PublicIpAddress") : resource_records
130
+ end
131
+
132
+ resource "#{name}Hostname",
133
+ Type: 'AWS::Route53::RecordSet',
134
+ Properties: properties
135
+ end
136
+
137
+ # [RESERVED] Create multiple record sets for given hosted zone
138
+ def create_multiple_dns_records
139
+ fail('method "create_multiple_dns_records" is not implemented')
140
+ end
141
+ end # module Route53
142
+ end # module Plugins
143
+ end # module Enscalator
@@ -0,0 +1,85 @@
1
+ module Enscalator
2
+ module Plugins
3
+ # Ubuntu appliance
4
+ module Ubuntu
5
+ class << self
6
+ # Supported storage types in AWS
7
+ STORAGE = [:ebs, :'ebs-io1', :'ebs-ssd', :'instance-store']
8
+
9
+ # Supported Ubuntu image architectures
10
+ ARCH = [:amd64, :i386]
11
+
12
+ # Supported Ubuntu releases
13
+ RELEASE = {
14
+ vivid: '15.04',
15
+ utopic: '14.10',
16
+ trusty: '14.04',
17
+ saucy: '13.10',
18
+ raring: '13.04',
19
+ quantal: '12.10',
20
+ precise: '12.04'
21
+ }
22
+
23
+ # Structure to hold parsed record
24
+ Struct.new('Ubuntu', :name, :edition, :state, :timestamp, :root_storage, :arch, :region, :ami, :virtualization)
25
+
26
+ # Get mapping for Ubuntu images
27
+ #
28
+ # @param [Symbol, String] release a codename or version number
29
+ # @param [Symbol] storage storage kind
30
+ # @param [Symbol] arch architecture
31
+ # @raise [ArgumentError] if release is nil, empty or not one of supported values
32
+ # @raise [ArgumentError] if storage is nil, empty or not one of supported values
33
+ # @raise [ArgumentError] if arch is nil, empty or not one of supported values
34
+ # @return [Hash] mapping for Ubuntu amis
35
+ def get_mapping(release: :trusty, storage: :ebs, arch: :amd64)
36
+ fail ArgumentError, 'release can be either codename or version' unless RELEASE.to_a.flatten.include? release
37
+ fail ArgumentError, "storage can only be one of #{STORAGE}" unless STORAGE.include? storage
38
+ fail ArgumentError, "arch can only be one of #{ARCH}" unless ARCH.include? arch
39
+ begin
40
+ version = RELEASE.keys.include?(release) ? release : RELEASE.key(release)
41
+ body = open("https://cloud-images.ubuntu.com/query/#{version}/server/released.current.txt") { |f| f.read }
42
+ body.split("\n").map { |m| m.squeeze("\t").split("\t").reject { |r| r.include? 'aki' } }
43
+ .map { |l| Struct::Ubuntu.new(*l) }
44
+ .select { |r| r.root_storage == storage.to_s && r.arch == arch.to_s }
45
+ .group_by(&:region)
46
+ .map { |k, v| [k, v.map { |i| [i.virtualization, i.ami] }.to_h] }
47
+ .to_h
48
+ .with_indifferent_access
49
+ end
50
+ end
51
+ end # class << self
52
+
53
+ # Create new Ubuntu instance
54
+ #
55
+ # @param [String] instance_name instance name
56
+ # @param [String] storage storage kind (ebs or ephemeral)
57
+ # @param [String] arch architecture (amd64 or i386)
58
+ # @param [String] instance_type instance type
59
+ def ubuntu_init(instance_name,
60
+ storage: :ebs,
61
+ arch: :amd64,
62
+ instance_type: 't2.medium', properties: {})
63
+
64
+ mapping 'AWSUbuntuAMI', Ubuntu.get_mapping(storage: storage, arch: arch)
65
+
66
+ parameter_allocated_storage "Ubuntu#{instance_name}",
67
+ default: 5,
68
+ min: 5,
69
+ max: 1024
70
+
71
+ parameter_ec2_instance_type "Ubuntu#{instance_name}", type: instance_type
72
+
73
+ instance_vpc "Ubuntu#{instance_name}",
74
+ find_in_map('AWSUbuntuAMI', ref('AWS::Region'), 'hvm'),
75
+ ref_application_subnets.first,
76
+ [ref_private_security_group, ref_application_security_group],
77
+ depends_on: [],
78
+ properties: {
79
+ KeyName: ref("Ubuntu#{instance_name}KeyName"),
80
+ InstanceType: ref("Ubuntu#{instance_name}InstanceType")
81
+ }.merge(properties)
82
+ end
83
+ end # Ubuntu
84
+ end # Plugins
85
+ end # Enscalator