cfn-vpn 1.3.1 → 1.4.1
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/docs/README.md +2 -1
- data/docs/certificate-users.md +1 -1
- data/docs/getting-started.md +56 -27
- data/docs/routes.md +48 -0
- data/docs/slack-notifications.md +35 -0
- data/lib/cfnvpn/actions/embedded.rb +3 -4
- data/lib/cfnvpn/actions/init.rb +9 -3
- data/lib/cfnvpn/actions/modify.rb +4 -2
- data/lib/cfnvpn/actions/revoke.rb +2 -3
- data/lib/cfnvpn/actions/routes.rb +20 -16
- data/lib/cfnvpn/actions/sessions.rb +4 -5
- data/lib/cfnvpn/actions/share.rb +3 -4
- data/lib/cfnvpn/actions/subnets.rb +2 -6
- data/lib/cfnvpn/clientvpn.rb +38 -22
- data/lib/cfnvpn/templates/lambdas/auto_route_populator/app.py +177 -92
- data/lib/cfnvpn/templates/lambdas/auto_route_populator/quotas.py +37 -0
- data/lib/cfnvpn/templates/lambdas/auto_route_populator/states.py +21 -0
- data/lib/cfnvpn/templates/lambdas/lib/slack.py +66 -0
- data/lib/cfnvpn/templates/lambdas/scheduler/app.py +42 -24
- data/lib/cfnvpn/templates/lambdas/scheduler/states.py +13 -0
- data/lib/cfnvpn/templates/lambdas.rb +10 -1
- data/lib/cfnvpn/templates/vpn.rb +86 -21
- data/lib/cfnvpn/version.rb +1 -1
- metadata +7 -2
@@ -1,26 +1,41 @@
|
|
1
|
+
import os
|
1
2
|
import socket
|
2
3
|
import boto3
|
3
4
|
from botocore.exceptions import ClientError
|
5
|
+
from lib.slack import Slack
|
6
|
+
from states import *
|
4
7
|
import logging
|
8
|
+
from quotas import increase_quota, AUTH_RULE_TABLE_QUOTA_CODE, ROUTE_TABLE_QUOTA_CODE
|
5
9
|
|
6
|
-
logger = logging.getLogger()
|
10
|
+
logger = logging.getLogger(__name__)
|
7
11
|
logger.setLevel(logging.INFO)
|
8
12
|
|
13
|
+
SLACK_USERNAME = 'CfnVpn Route Table Event'
|
9
14
|
|
10
15
|
def delete_route(client, vpn_endpoint, subnet, cidr):
|
16
|
+
try:
|
11
17
|
client.delete_client_vpn_route(
|
12
18
|
ClientVpnEndpointId=vpn_endpoint,
|
13
19
|
TargetVpcSubnetId=subnet,
|
14
20
|
DestinationCidrBlock=cidr,
|
15
21
|
)
|
22
|
+
except ClientError as e:
|
23
|
+
if e.response['Error']['Code'] == 'InvalidClientVpnEndpointAuthorizationRuleNotFound':
|
24
|
+
logger.info(f"route not found when deleting", exc_info=True)
|
25
|
+
else:
|
26
|
+
raise e
|
16
27
|
|
17
28
|
|
18
|
-
def create_route(client, event, cidr):
|
29
|
+
def create_route(client, event, cidr, target_subnet):
|
30
|
+
description = f"cfnvpn auto generated route for endpoint {event['Record']}."
|
31
|
+
if event['Description']:
|
32
|
+
description += f" {event['Description']}"
|
33
|
+
|
19
34
|
client.create_client_vpn_route(
|
20
35
|
ClientVpnEndpointId=event['ClientVpnEndpointId'],
|
21
36
|
DestinationCidrBlock=cidr,
|
22
|
-
TargetVpcSubnetId=
|
23
|
-
Description=
|
37
|
+
TargetVpcSubnetId=target_subnet,
|
38
|
+
Description=description
|
24
39
|
)
|
25
40
|
|
26
41
|
|
@@ -34,15 +49,27 @@ def revoke_route_auth(client, event, cidr, group = None):
|
|
34
49
|
args['RevokeAllGroups'] = True
|
35
50
|
else:
|
36
51
|
args['AccessGroupId'] = group
|
37
|
-
|
38
|
-
|
52
|
+
|
53
|
+
try:
|
54
|
+
client.revoke_client_vpn_ingress(**args)
|
55
|
+
except ClientError as e:
|
56
|
+
if e.response['Error']['Code'] == 'ConcurrentMutationLimitExceeded':
|
57
|
+
logger.warn(f"revoking auth is being rate limited", exc_info=True)
|
58
|
+
elif e.response['Error']['Code'] == 'InvalidClientVpnEndpointAuthorizationRuleNotFound':
|
59
|
+
logger.info(f"rule not found when revoking", exc_info=True)
|
60
|
+
else:
|
61
|
+
raise e
|
39
62
|
|
40
63
|
|
41
64
|
def authorize_route(client, event, cidr, group = None):
|
65
|
+
description = f"cfnvpn auto generated authorization for endpoint {event['Record']}."
|
66
|
+
if event['Description']:
|
67
|
+
description += f" {event['Description']}"
|
68
|
+
|
42
69
|
args = {
|
43
70
|
'ClientVpnEndpointId': event['ClientVpnEndpointId'],
|
44
71
|
'TargetNetworkCidr': cidr,
|
45
|
-
'Description':
|
72
|
+
'Description': description
|
46
73
|
}
|
47
74
|
|
48
75
|
if group is None:
|
@@ -54,7 +81,8 @@ def authorize_route(client, event, cidr, group = None):
|
|
54
81
|
|
55
82
|
|
56
83
|
def get_routes(client, event):
|
57
|
-
|
84
|
+
paginator = client.get_paginator('describe_client_vpn_routes')
|
85
|
+
response_iterator = paginator.paginate(
|
58
86
|
ClientVpnEndpointId=event['ClientVpnEndpointId'],
|
59
87
|
Filters=[
|
60
88
|
{
|
@@ -63,113 +91,170 @@ def get_routes(client, event):
|
|
63
91
|
}
|
64
92
|
]
|
65
93
|
)
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
94
|
+
|
95
|
+
return [route for page in response_iterator
|
96
|
+
for route in page['Routes']
|
97
|
+
if 'Description' in route and event['Record'] in route['Description']]
|
70
98
|
|
71
99
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
{
|
77
|
-
'Name': 'destination-cidr',
|
78
|
-
'Values': [cidr]
|
79
|
-
}
|
80
|
-
]
|
100
|
+
def get_auth_rules(client, event):
|
101
|
+
paginator = client.get_paginator('describe_client_vpn_authorization_rules')
|
102
|
+
response_iterator = paginator.paginate(
|
103
|
+
ClientVpnEndpointId=event['ClientVpnEndpointId']
|
81
104
|
)
|
82
|
-
|
105
|
+
|
106
|
+
return [rule for page in response_iterator
|
107
|
+
for rule in page['AuthorizationRules']
|
108
|
+
if 'Description' in rule and event['Record'] in rule['Description']]
|
109
|
+
|
110
|
+
|
111
|
+
def expired_auth_rules(auth_rules, cidrs, groups):
|
112
|
+
for rule in auth_rules:
|
113
|
+
# if there is a rule for the record with an old cidr
|
114
|
+
if rule['DestinationCidr'] not in cidrs:
|
115
|
+
yield rule
|
116
|
+
# if there is a rule for a group that is no longer in the event
|
117
|
+
if groups and rule['GroupId'] not in groups:
|
118
|
+
yield rule
|
119
|
+
# if there is a rule for allow all but groups are in the event
|
120
|
+
if groups and rule['AccessAll']:
|
121
|
+
yield rule
|
122
|
+
|
123
|
+
|
124
|
+
def expired_routes(routes, cidrs):
|
125
|
+
for route in routes:
|
126
|
+
if route['DestinationCidr'] not in cidrs:
|
127
|
+
yield route
|
83
128
|
|
84
129
|
|
85
130
|
def handler(event,context):
|
131
|
+
|
132
|
+
logger.info(f"auto route populator triggered with event : {event}")
|
133
|
+
slack = Slack(username=SLACK_USERNAME)
|
86
134
|
|
87
135
|
# DNS lookup on the dns record and return all IPS for the endpoint
|
88
136
|
try:
|
89
137
|
cidrs = [ ip + "/32" for ip in socket.gethostbyname_ex(event['Record'])[-1]]
|
90
138
|
logger.info(f"resolved endpoint {event['Record']} to {cidrs}")
|
91
139
|
except socket.gaierror as e:
|
92
|
-
logger.
|
140
|
+
logger.error(f"failed to resolve record {event['Record']}", exc_info=True)
|
141
|
+
slack.post_event(message=f"failed to resolve record {event['Record']}", state=RESOLVE_FAILED, error=e)
|
93
142
|
return 'KO'
|
94
143
|
|
95
144
|
client = boto3.client('ec2')
|
145
|
+
|
146
|
+
# describe vpn and check if subnets are associated with the vpn
|
147
|
+
response = client.describe_client_vpn_endpoints(
|
148
|
+
ClientVpnEndpointIds=[event['ClientVpnEndpointId']]
|
149
|
+
)
|
150
|
+
|
151
|
+
if not response['ClientVpnEndpoints']:
|
152
|
+
logger.error(f"endpoint not found")
|
153
|
+
slack.post_event(message=f"failed create routes for {event['Record']}", state=FAILED, error="endpoint not found")
|
154
|
+
return 'KO'
|
155
|
+
|
156
|
+
endpoint = response['ClientVpnEndpoints'][0]
|
157
|
+
if endpoint['Status'] == 'pending-associate':
|
158
|
+
logger.error(f"no subnets associated with endpoint")
|
159
|
+
slack.post_event(message=f"failed create routes for {event['Record']}", state=FAILED, error="vpn is in a stopped state")
|
160
|
+
return 'KO'
|
161
|
+
|
96
162
|
routes = get_routes(client, event)
|
163
|
+
auth_rules = get_auth_rules(client, event)
|
164
|
+
|
165
|
+
auto_limit_increase = os.environ.get('AUTO_LIMIT_INCREASE')
|
166
|
+
route_limit_increase_required = False
|
167
|
+
auth_rules_limit_increase_required = False
|
97
168
|
|
98
169
|
for cidr in cidrs:
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
170
|
+
# create route if doesn't exist
|
171
|
+
for subnet in event['TargetSubnets']:
|
172
|
+
if not any(route['DestinationCidr'] == cidr and route['TargetSubnet'] == subnet for route in routes):
|
173
|
+
try:
|
174
|
+
create_route(client, event, cidr, subnet)
|
175
|
+
except ClientError as e:
|
176
|
+
if e.response['Error']['Code'] == 'ClientVpnRouteLimitExceeded':
|
177
|
+
route_limit_increase_required = True
|
178
|
+
logger.error("vpn route table has reached the route limit", exc_info=True)
|
179
|
+
slack.post_event(
|
180
|
+
message=f"unable to create route {cidr} from {event['Record']}",
|
181
|
+
state=ROUTE_LIMIT_EXCEEDED,
|
182
|
+
error="vpn route table has reached the route limit"
|
183
|
+
)
|
184
|
+
elif e.response['Error']['Code'] == 'InvalidClientVpnActiveAssociationNotFound':
|
185
|
+
logger.warn("no subnets are associated with the vpn", exc_info=True)
|
186
|
+
slack.post_event(
|
187
|
+
message=f"unable to create the route {cidr} from {event['Record']}",
|
188
|
+
state=SUBNET_NOT_ASSOCIATED,
|
189
|
+
error="no subnets are associated with the vpn"
|
190
|
+
)
|
191
|
+
else:
|
192
|
+
logger.error("encountered a unexpected client error when creating a route", exc_info=True)
|
108
193
|
else:
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
else:
|
118
|
-
|
119
|
-
logger.info(f"route for cidr {cidr} is already in place")
|
120
|
-
|
121
|
-
# if the target subnet has changed in the payload, recreate the routes to use the new subnet
|
122
|
-
if route['TargetSubnet'] != event['TargetSubnet']:
|
123
|
-
logger.info(f"target subnet for route for {cidr} has changed, recreating the route")
|
194
|
+
slack.post_event(
|
195
|
+
message=f"created new route {cidr} ({event['Record']}) to target subnet {subnet}",
|
196
|
+
state=NEW_ROUTE
|
197
|
+
)
|
198
|
+
|
199
|
+
# remove route if target subnet has changed
|
200
|
+
for route in routes:
|
201
|
+
if route['DestinationCidr'] == cidr and route['TargetSubnet'] not in event['TargetSubnets']:
|
124
202
|
delete_route(client, event['ClientVpnEndpointId'], route['TargetSubnet'], cidr)
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
rules
|
131
|
-
existing_groups = [rule['GroupId'] for rule in rules]
|
203
|
+
|
204
|
+
# collect all rules that matches the current cidr
|
205
|
+
cidr_auth_rules = [rule for rule in auth_rules if rule['DestinationCidr'] == cidr]
|
206
|
+
|
207
|
+
try:
|
208
|
+
# create rules for newly added groups
|
132
209
|
if 'Groups' in event:
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
revoke_route_auth(client, event, cidr, rule['GroupId'])
|
138
|
-
# add new rules defined in the payload
|
139
|
-
new_rules = [group for group in event['Groups'] if group not in existing_groups]
|
140
|
-
for group in new_rules:
|
141
|
-
logger.info(f"creating new authorization rule for group {rule['GroupId']} for route {cidr}")
|
210
|
+
existing_groups = list(set(rule['GroupId'] for rule in cidr_auth_rules))
|
211
|
+
new_groups = [group for group in event['Groups'] if group not in existing_groups]
|
212
|
+
|
213
|
+
for group in new_groups:
|
142
214
|
authorize_route(client, event, cidr, group)
|
143
|
-
else:
|
144
|
-
# if amount of rules for the cidr is greater than 1 when no groups are specified in the payload
|
145
|
-
# we'll assume that all groups have been removed from the payload so we'll remove all existing rules and add a rule for allow all
|
146
|
-
if len(rules) > 1:
|
147
|
-
logger.info(f"creating an allow all rule for route {cidr}")
|
148
|
-
revoke_route_auth(client, event, cidr)
|
149
|
-
authorize_route(client, event, cidr)
|
150
|
-
|
151
|
-
|
152
215
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
logger.info(f"removing expired route {route['DestinationCidr']} for endpoint {event['Record']}")
|
158
|
-
|
159
|
-
try:
|
160
|
-
revoke_route_auth(client, event, route['DestinationCidr'])
|
161
|
-
except ClientError as e:
|
162
|
-
if e.response['Error']['Code'] == 'InvalidClientVpnEndpointAuthorizationRuleNotFound':
|
163
|
-
pass
|
164
|
-
else:
|
165
|
-
raise e
|
166
|
-
|
167
|
-
try:
|
168
|
-
delete_route(client, event['ClientVpnEndpointId'], route['TargetSubnet'], route['DestinationCidr'])
|
216
|
+
# create an allow all rule
|
217
|
+
elif 'Groups' not in event and not cidr_auth_rules:
|
218
|
+
authorize_route(client, event, cidr)
|
219
|
+
|
169
220
|
except ClientError as e:
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
221
|
+
if e.response['Error']['Code'] == 'ClientVpnAuthorizationRuleLimitExceeded':
|
222
|
+
auth_rules_limit_increase_required = True
|
223
|
+
logger.error("vpn has reached the authorization rule limit", exc_info=True)
|
224
|
+
slack.post_event(
|
225
|
+
message=f"unable add to authorization rule for route {cidr} from {event['Record']}",
|
226
|
+
state=AUTH_RULE_LIMIT_EXCEEDED,
|
227
|
+
error="vpn has reached the authorization rule limit"
|
228
|
+
)
|
229
|
+
continue
|
230
|
+
else:
|
231
|
+
logger.error("encountered a unexpected client error when creating an auth rule", exc_info=True)
|
232
|
+
|
233
|
+
# request route limit increase
|
234
|
+
if route_limit_increase_required and auto_limit_increase:
|
235
|
+
case_id = increase_quota(10, ROUTE_TABLE_QUOTA_CODE, event['ClientVpnEndpointId'])
|
236
|
+
if case_id is not None:
|
237
|
+
slack.post_event(message=f"requested an increase for the routes per vpn service quota", state=QUOTA_INCREASE_REQUEST, support_case=case_id)
|
238
|
+
else:
|
239
|
+
logger.info(f"routes per vpn service quota increase request pending")
|
240
|
+
|
241
|
+
# request auth rule limit increase
|
242
|
+
if auth_rules_limit_increase_required and auto_limit_increase:
|
243
|
+
case_id = increase_quota(20, AUTH_RULE_TABLE_QUOTA_CODE, event['ClientVpnEndpointId'])
|
244
|
+
if case_id is not None:
|
245
|
+
slack.post_event(message=f"requested an increase for the authorization rules per vpn service quota", state=QUOTA_INCREASE_REQUEST, support_case=case_id)
|
246
|
+
else:
|
247
|
+
logger.info(f"authorization rules per vpn service quota increase request pending")
|
248
|
+
|
249
|
+
# remove expired auth rules
|
250
|
+
for rule in expired_auth_rules(auth_rules, cidrs, event.get('Groups', [])):
|
251
|
+
logger.info(f"removing expired auth rule {rule['DestinationCidr']} for endpoint {event['Record']}")
|
252
|
+
revoke_route_auth(client, event, route['DestinationCidr'])
|
253
|
+
|
254
|
+
# remove expired routes
|
255
|
+
for route in expired_routes(routes, cidrs):
|
256
|
+
logger.info(f"removing expired route {route['DestinationCidr']} for endpoint {event['Record']}")
|
257
|
+
delete_route(client, event['ClientVpnEndpointId'], route['TargetSubnet'], route['DestinationCidr'])
|
258
|
+
slack.post_event(message=f"removed expired route {route['DestinationCidr']} for endpoint {event['Record']}", state=EXPIRED_ROUTE)
|
174
259
|
|
175
260
|
return 'OK'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import boto3
|
2
|
+
|
3
|
+
ROUTE_TABLE_QUOTA_CODE = 'L-401D78F7'
|
4
|
+
AUTH_RULE_TABLE_QUOTA_CODE = 'L-9A1BC94B'
|
5
|
+
EC2_SERVICE_CODE = 'ec2'
|
6
|
+
IN_PROGRESS = ['PENDING', 'CASE_OPENED']
|
7
|
+
|
8
|
+
def get_route_count(endpoint) -> int:
|
9
|
+
client = boto3.client('ec2')
|
10
|
+
response = client.describe_client_vpn_routes(
|
11
|
+
ClientVpnEndpointId=endpoint,
|
12
|
+
)
|
13
|
+
return len(response['Routes'])
|
14
|
+
|
15
|
+
def quota_request_open(quota_code) -> bool:
|
16
|
+
client = boto3.client('service-quotas')
|
17
|
+
response = client.list_requested_service_quota_change_history_by_quota(
|
18
|
+
ServiceCode=EC2_SERVICE_CODE,
|
19
|
+
QuotaCode=quota_code
|
20
|
+
)
|
21
|
+
# Status='PENDING'|'CASE_OPENED'|'APPROVED'|'DENIED'|'CASE_CLOSED'
|
22
|
+
return any(req['status'] in IN_PROGRESS for req in response['RequestedQuotas'])
|
23
|
+
|
24
|
+
def increase_quota(increase_value, quota_code, endpoint) -> str:
|
25
|
+
if quota_request_open(quota_code):
|
26
|
+
return None
|
27
|
+
|
28
|
+
current_route_count = get_route_count(endpoint)
|
29
|
+
desired_value = current_route_count + increase_value
|
30
|
+
|
31
|
+
client = boto3.client('service-quotas')
|
32
|
+
response = client.request_service_quota_increase(
|
33
|
+
ServiceCode=EC2_SERVICE_CODE,
|
34
|
+
QuotaCode=quota_code,
|
35
|
+
DesiredValue=desired_value
|
36
|
+
)
|
37
|
+
return response['CaseId']
|
@@ -0,0 +1,21 @@
|
|
1
|
+
"""
|
2
|
+
states:
|
3
|
+
|
4
|
+
FAILED: general failure
|
5
|
+
NEW_ROUTE: new route added to route table
|
6
|
+
EXPIRED_ROUTE: cidr is no longer associated with DNS entry and is removed from the route table
|
7
|
+
ROUTE_LIMIT_EXCEEDED: no new routes can be added to the route table due to aws route table limit
|
8
|
+
AUTH_RULE_LIMIT_EXCEEDED: no new authorization rules can be added to the rule list due to aws auth rule limit
|
9
|
+
RESOLVE_FAILED: failed to resolve the provided dns entry
|
10
|
+
SUBNET_NOT_ASSOCIATED: no subnets are associated with the client vpn
|
11
|
+
QUOTA_INCREASE_REQUEST: automatic quota increase made
|
12
|
+
"""
|
13
|
+
|
14
|
+
FAILED = 'FAILED'
|
15
|
+
NEW_ROUTE = 'NEW_ROUTE'
|
16
|
+
EXPIRED_ROUTE = 'EXPIRED_ROUTE'
|
17
|
+
ROUTE_LIMIT_EXCEEDED = 'ROUTE_LIMIT_EXCEEDED'
|
18
|
+
AUTH_RULE_LIMIT_EXCEEDED = 'AUTH_RULE_LIMIT_EXCEEDED'
|
19
|
+
RESOLVE_FAILED = 'RESOLVE_FAILED'
|
20
|
+
SUBNET_NOT_ASSOCIATED = 'SUBNET_NOT_ASSOCIATED'
|
21
|
+
QUOTA_INCREASE_REQUEST = 'QUOTA_INCREASE_REQUEST'
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import urllib
|
5
|
+
|
6
|
+
logger = logging.getLogger(__name__)
|
7
|
+
logger.setLevel(logging.INFO)
|
8
|
+
|
9
|
+
class Slack:
|
10
|
+
|
11
|
+
def __init__(self, username):
|
12
|
+
self.username = username
|
13
|
+
self.slack_url = os.environ.get('SLACK_URL')
|
14
|
+
|
15
|
+
def post_event(self, message, state, error=None, support_case=None):
|
16
|
+
"""Posts event to slack using an incoming webhook
|
17
|
+
Parameters
|
18
|
+
----------
|
19
|
+
message: str
|
20
|
+
message to post to slack
|
21
|
+
state: str
|
22
|
+
the state of the event
|
23
|
+
error: str
|
24
|
+
error message to add to the message
|
25
|
+
support_case: str
|
26
|
+
displays a aws console link to the support case in the message
|
27
|
+
"""
|
28
|
+
|
29
|
+
if not self.slack_url.startswith('https://hooks.slack.com'):
|
30
|
+
return
|
31
|
+
|
32
|
+
if 'FAILED' in state or 'LIMIT_EXCEEDED' in state:
|
33
|
+
colour = '#ad0614'
|
34
|
+
elif 'NOT_ASSOCIATED' in state:
|
35
|
+
colour = '#d4b126'
|
36
|
+
else:
|
37
|
+
colour = '#3ead3e'
|
38
|
+
|
39
|
+
text = f'Message: {message}\nState: {state}'
|
40
|
+
|
41
|
+
if error:
|
42
|
+
text += f'\nError: {error}'
|
43
|
+
|
44
|
+
if support_case:
|
45
|
+
text += f'\nSupport Case: <https://console.aws.amazon.com/support/cases#/{support_case}|{support_case}>'
|
46
|
+
|
47
|
+
payload = {
|
48
|
+
'username': self.username,
|
49
|
+
'attachments': [
|
50
|
+
{
|
51
|
+
'color': colour,
|
52
|
+
'text': text,
|
53
|
+
'mrkdwn_in': ['text','pretext']
|
54
|
+
}
|
55
|
+
]
|
56
|
+
}
|
57
|
+
|
58
|
+
try:
|
59
|
+
urllib.request.urlopen(urllib.request.Request(
|
60
|
+
self.slack_url,
|
61
|
+
headers={'Content-Type': 'application/json'},
|
62
|
+
data=json.dumps(payload).encode('utf-8')
|
63
|
+
))
|
64
|
+
except urllib.error.HTTPError as e:
|
65
|
+
logger.error(f"failed to post slack notification. REASON: {e.reason} CODE: {e.code}", exc_info=True)
|
66
|
+
|
@@ -1,36 +1,54 @@
|
|
1
1
|
import boto3
|
2
2
|
import logging
|
3
|
+
from lib.slack import Slack
|
4
|
+
from states import *
|
3
5
|
|
4
6
|
logger = logging.getLogger()
|
5
7
|
logger.setLevel(logging.INFO)
|
6
8
|
|
9
|
+
SLACK_USERNAME = 'CfnVpn Scheduler'
|
10
|
+
|
7
11
|
def handler(event, context):
|
8
12
|
|
9
13
|
logger.info(f"updating cfn-vpn stack {event['StackName']} parameter AssociateSubnets with value {event['AssociateSubnets']}")
|
14
|
+
slack = Slack(username=SLACK_USERNAME)
|
15
|
+
|
16
|
+
try:
|
17
|
+
if event['AssociateSubnets'] == 'false':
|
18
|
+
logger.info(f"terminating current vpn sessions to {event['ClientVpnEndpointId']}")
|
19
|
+
ec2 = boto3.client('ec2')
|
20
|
+
resp = ec2.describe_client_vpn_connections(ClientVpnEndpointId=event['ClientVpnEndpointId'])
|
21
|
+
for conn in resp['Connections']:
|
22
|
+
if conn['Status']['Code'] == 'active':
|
23
|
+
ec2.terminate_client_vpn_connections(
|
24
|
+
ClientVpnEndpointId=event['ClientVpnEndpointId'],
|
25
|
+
ConnectionId=conn['ConnectionId']
|
26
|
+
)
|
27
|
+
logger.info(f"terminated session {conn['ConnectionId']}")
|
28
|
+
|
29
|
+
client = boto3.client('cloudformation')
|
30
|
+
logger.info(client.update_stack(
|
31
|
+
StackName=event['StackName'],
|
32
|
+
UsePreviousTemplate=True,
|
33
|
+
Capabilities=['CAPABILITY_IAM'],
|
34
|
+
Parameters=[
|
35
|
+
{
|
36
|
+
'ParameterKey': 'AssociateSubnets',
|
37
|
+
'ParameterValue': event['AssociateSubnets']
|
38
|
+
}
|
39
|
+
]
|
40
|
+
))
|
41
|
+
except Exception as ex:
|
42
|
+
logger.error(f"failed to start/stop client vpn", exc_info=True)
|
43
|
+
if event['AssociateSubnets'] == 'true':
|
44
|
+
slack.post_event(message=f"failed to associate subnets with the client vpn", state=START_FAILED, error=ex)
|
45
|
+
else:
|
46
|
+
slack.post_event(message=f"failed to disassociate subnets with the client vpn", state=STOP_FAILED, error=ex)
|
47
|
+
return 'KO'
|
10
48
|
|
11
|
-
if event['AssociateSubnets'] == '
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
for conn in resp['Connections']:
|
16
|
-
if conn['Status']['Code'] == 'active':
|
17
|
-
ec2.terminate_client_vpn_connections(
|
18
|
-
ClientVpnEndpointId=event['ClientVpnEndpointId'],
|
19
|
-
ConnectionId=conn['ConnectionId']
|
20
|
-
)
|
21
|
-
logger.info(f"terminated session {conn['ConnectionId']}")
|
22
|
-
|
23
|
-
client = boto3.client('cloudformation')
|
24
|
-
logger.info(client.update_stack(
|
25
|
-
StackName=event['StackName'],
|
26
|
-
UsePreviousTemplate=True,
|
27
|
-
Capabilities=['CAPABILITY_IAM'],
|
28
|
-
Parameters=[
|
29
|
-
{
|
30
|
-
'ParameterKey': 'AssociateSubnets',
|
31
|
-
'ParameterValue': event['AssociateSubnets']
|
32
|
-
}
|
33
|
-
]
|
34
|
-
))
|
49
|
+
if event['AssociateSubnets'] == 'true':
|
50
|
+
slack.post_event(message=f"successfully associated subnets with the client vpn", state=START_IN_PROGRESS)
|
51
|
+
else:
|
52
|
+
slack.post_event(message=f"successfully disassociated subnets with the client vpn", state=STOP_IN_PROGRESS)
|
35
53
|
|
36
54
|
return 'OK'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""
|
2
|
+
states:
|
3
|
+
|
4
|
+
START_IN_PROGRESS: associating subnets with the Client VPN
|
5
|
+
STOP_IN_PROGRESS: disassociating subnets with the Client VPN
|
6
|
+
START_FAILED: failed to associated subnets with the Client VPN
|
7
|
+
STOP_FAILED: failed to disassociated subnets with the Client VPN
|
8
|
+
"""
|
9
|
+
|
10
|
+
START_IN_PROGRESS = 'START_IN_PROGRESS'
|
11
|
+
STOP_IN_PROGRESS = 'STOP_IN_PROGRESS'
|
12
|
+
START_FAILED = 'START_FAILED'
|
13
|
+
STOP_FAILED = 'STOP_FAILED'
|
@@ -17,7 +17,16 @@ module CfnVpn
|
|
17
17
|
FileUtils.mkdir_p(zipfile_path)
|
18
18
|
Zip::File.open("#{zipfile_path}/#{zipfile_name}", Zip::File::CREATE) do |zipfile|
|
19
19
|
files.each do |file|
|
20
|
-
|
20
|
+
file_path = lambdas_dir
|
21
|
+
|
22
|
+
# this is to allow for shared library methods in a different lambda directory
|
23
|
+
# so the files in the function lambda is in the root of the zip
|
24
|
+
if file.include? func
|
25
|
+
file_path = "#{file_path}/#{func}"
|
26
|
+
file.gsub!("#{func}/", "")
|
27
|
+
end
|
28
|
+
|
29
|
+
zipfile.add(file, File.join(file_path, file))
|
21
30
|
end
|
22
31
|
end
|
23
32
|
|