cfn-vpn 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +49 -1
- 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 +22 -18
- 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 +88 -23
- data/lib/cfnvpn/version.rb +1 -1
- metadata +8 -3
@@ -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 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 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
|
|