UNPKG

12.5 kBJavaScriptView Raw
1'use strict';
2
3const fs = require('fs-extra');
4const tpl = require('./tpl');
5const path = require('path');
6const debug = require('debug')('fun:nas');
7const constants = require('./nas/constants');
8const definition = require('./definition');
9const getProfile = require('./profile').getProfile;
10const getAvailableVSwitchId = require('./vswitch').getAvailableVSwitchId;
11
12const { green } = require('colors');
13const { sleep } = require('./time');
14const { getNasPopClient, getVpcPopClient } = require('./client');
15const { throwProcessedException } = require('./error-message');
16
17const _ = require('lodash');
18
19const requestOption = {
20 method: 'POST'
21};
22
23const NAS_DEFAULT_DESCRIPTION = 'default_nas_created_by_fc_fun';
24
25async function createMountTarget(nasClient, region, fileSystemId, vpcId, vswitchId) {
26 const params = {
27 'RegionId': region,
28 'NetworkType': 'Vpc',
29 'FileSystemId': fileSystemId,
30 'AccessGroupName': 'DEFAULT_VPC_GROUP_NAME',
31 'VpcId': vpcId,
32 'VSwitchId': vswitchId
33 };
34
35 const rs = await nasClient.request('CreateMountTarget', params, requestOption);
36
37 const mountTargetDomain = rs.MountTargetDomain;
38
39 debug('create mount target rs: %s', mountTargetDomain);
40
41 await waitMountPointUntilAvaliable(nasClient, region, fileSystemId, mountTargetDomain);
42
43 return mountTargetDomain;
44}
45
46async function waitMountPointUntilAvaliable(nasClient, region, fileSystemId, mountTargetDomain) {
47 let count = 0;
48 let status;
49
50 do {
51 count++;
52
53 var params = {
54 'RegionId': region,
55 'FileSystemId': fileSystemId,
56 'MountTargetDomain': mountTargetDomain
57 };
58
59 await sleep(800);
60
61 const rs = await nasClient.request('DescribeMountTargets', params, requestOption);
62 status = rs.MountTargets.MountTarget[0].Status;
63
64 debug('nas status is: ' + status);
65
66 console.log(`\t\tnas mount target domain already created, waiting for status to be 'Active', now is ${status}`);
67 } while (count < 15 && status !== 'Active');
68
69 if (status !== 'Active') { throw new Error(`Timeout while waiting for MountPoint ${mountTargetDomain} status to be 'Active'`); }
70}
71
72async function createDefaultNasIfNotExist(vpcId, vswitchIds) {
73 const nasClient = await getNasPopClient();
74 const vpcClient = await getVpcPopClient();
75
76 const profile = await getProfile();
77 const region = profile.defaultRegion;
78
79 const nasZones = await describeNasZones(nasClient, region);
80
81 const { zoneId, vswitchId, storageType } = await getAvailableVSwitchId(vpcClient, region, vswitchIds, nasZones);
82
83 const fileSystemId = await createNasFileSystemIfNotExist(nasClient, region, zoneId, storageType);
84
85 debug('fileSystemId: %s', fileSystemId);
86
87 return await createMountTargetIfNotExist(nasClient, region, fileSystemId, vpcId, vswitchId);
88}
89
90async function findMountTarget(nasClient, region, fileSystemId, vpcId, vswitchId) {
91 var params = {
92 'RegionId': region,
93 'FileSystemId': fileSystemId
94 };
95
96 const rs = await nasClient.request('DescribeMountTargets', params, requestOption);
97
98 const mountTargets = rs.MountTargets.MountTarget;
99
100 // todo: 检查 mountTargets 的 vswitch 是否与函数计算的一致?
101
102 if (!_.isEmpty(mountTargets)) {
103
104 const mountTarget = _.find(mountTargets, {
105 'VpcId': vpcId,
106 'VswId': vswitchId
107 });
108
109 if (mountTarget) {
110 return mountTarget.MountTargetDomain;
111 }
112 }
113
114 return null;
115}
116
117async function createMountTargetIfNotExist(nasClient, region, fileSystemId, vpcId, vswitchId) {
118
119 let mountTargetDomain = await findMountTarget(nasClient, region, fileSystemId, vpcId, vswitchId);
120
121 if (mountTargetDomain) {
122 console.log(green('\t\tnas file system mount target is already created, mountTargetDomain is: ' + mountTargetDomain));
123
124 return mountTargetDomain;
125 }
126
127 // create mountTarget if not exist
128
129 console.log('\t\tcould not find default nas file system mount target, ready to generate one');
130
131 mountTargetDomain = await createMountTarget(nasClient, region, fileSystemId, vpcId, vswitchId);
132
133 console.log(green('\t\tdefault nas file system mount target has been generated, mount domain is: ' + mountTargetDomain));
134
135 return mountTargetDomain;
136}
137
138async function createNasFileSystemIfNotExist(nasClient, region, zoneId, storageType) {
139 let fileSystemId = await findNasFileSystem(nasClient, region, NAS_DEFAULT_DESCRIPTION);
140
141 if (!fileSystemId) {
142 console.log('\t\tcould not find default nas file system, ready to generate one');
143
144 fileSystemId = await createNasFileSystem({ nasClient, region, zoneId, storageType });
145
146 console.log(green('\t\tdefault nas file system has been generated, fileSystemId is: ' + fileSystemId));
147 } else {
148 console.log(green('\t\tnas file system already generated, fileSystemId is: ' + fileSystemId));
149 }
150
151 return fileSystemId;
152}
153
154async function findNasFileSystem(nasClient, region, description) {
155
156 const pageSize = 50;
157 let requestPageNumber = 0;
158 let totalCount;
159 let pageNumber;
160
161 let fileSystem;
162
163 do {
164 const params = {
165 'RegionId': region,
166 'PageSize': pageSize,
167 'PageNumber': ++requestPageNumber
168 };
169
170 var rs;
171 try {
172 rs = await nasClient.request('DescribeFileSystems', params, requestOption);
173 } catch (ex) {
174 throwProcessedException(ex, 'AliyunNASFullAccess');
175 }
176
177 totalCount = rs.TotalCount;
178 pageNumber = rs.PageNumber;
179
180 const fileSystems = rs.FileSystems.FileSystem;
181
182 fileSystem = _.find(fileSystems, { Description: description });
183
184 debug('find filesystem: ' + JSON.stringify(fileSystem));
185
186 } while (!fileSystem && totalCount && pageNumber && pageNumber * pageSize < totalCount);
187
188 return (fileSystem || {}).FileSystemId;
189}
190
191async function createNasFileSystem({
192 nasClient,
193 region,
194 storageType,
195 zoneId
196}) {
197 const params = {
198 'RegionId': region,
199 'ProtocolType': 'NFS',
200 'StorageType': storageType,
201 'Description': NAS_DEFAULT_DESCRIPTION,
202 'ZoneId': zoneId
203 };
204
205 const rs = await nasClient.request('CreateFileSystem', params, requestOption);
206
207 return rs.FileSystemId;
208}
209
210async function generateAutoNasConfig(serviceName, vpcId, vswitchIds, userId, groupId) {
211 const mountPointDomain = await createDefaultNasIfNotExist(vpcId, vswitchIds);
212
213 //fun nas 创建的服务名比其对应的服务多了 '_FUN_NAS_' 前缀
214 //对于 nas 的挂载目录,要去掉这个前缀,保证 fun nas 的服务与对应的服务使用的是同样的挂载目录
215 if (serviceName.startsWith(constants.FUN_NAS_SERVICE_PREFIX)) {
216 serviceName = serviceName.substring(constants.FUN_NAS_SERVICE_PREFIX.length);
217 }
218 return {
219 UserId: userId || 10003,
220 GroupId: groupId || 10003,
221 MountPoints: [
222 {
223 ServerAddr: `${mountPointDomain}:/${serviceName}`,
224 MountDir: '/mnt/auto'
225 }
226 ]
227 };
228}
229
230const serverAddrReGe = /^[a-z0-9-.]*.nas.[a-z]+.com:\//;
231
232function resolveMountPoint(mountPoint) {
233 // '012194b28f-ujc20.cn-hangzhou.nas.aliyuncs.com:/'
234 const serverAddr = mountPoint.ServerAddr;
235 const mountDir = mountPoint.MountDir;
236
237 // valid serverAddr
238 if (!serverAddrReGe.test(serverAddr)) {
239 throw new Error(`NasConfig's nas server address '${serverAddr}' doesn't match expected format (allowed: '^[a-z0-9-.]*.nas.[a-z]+.com:/')`);
240 }
241
242 const suffix = '.com:';
243 const index = serverAddr.lastIndexOf(suffix);
244
245 // /
246 let mountSource = serverAddr.substr(index + suffix.length);
247 // 012194b28f-ujc20.cn-hangzhou.nas.aliyuncs.com
248 let serverPath = serverAddr.substr(0, serverAddr.length - mountSource.length - 1);
249
250 return {
251 serverPath,
252 mountSource,
253 mountDir,
254 serverAddr
255 };
256}
257
258async function convertMountPointToNasMapping(nasBaseDir, mountPoint) {
259 const { mountSource, mountDir, serverPath } = resolveMountPoint(mountPoint);
260
261 const nasDir = path.join(nasBaseDir, serverPath);
262
263 if (!(await fs.pathExists(nasDir))) {
264 await fs.ensureDir(nasDir);
265 }
266
267 const localNasDir = path.join(nasDir, mountSource);
268
269 // The mounted nas directory must exist.
270 if (!(await fs.pathExists(localNasDir))) {
271 await fs.ensureDir(localNasDir);
272 }
273
274 return {
275 localNasDir,
276 remoteNasDir: mountDir
277 };
278}
279
280async function convertMountPointsToNasMappings(nasBaseDir, mountPoints) {
281 if (!mountPoints) { return []; }
282
283 const nasMappings = [];
284
285 for (let mountPoint of mountPoints) {
286 const nasMapping = await convertMountPointToNasMapping(nasBaseDir, mountPoint);
287
288 nasMappings.push(nasMapping);
289 }
290
291 return nasMappings;
292}
293
294async function convertNasConfigToNasMappings(nasBaseDir, nasConfig, serviceName) {
295 if (!nasConfig) { return []; }
296
297 const isNasAuto = definition.isNasAutoConfig(nasConfig);
298
299 if (isNasAuto) { // support 'NasConfig: Auto'
300 const nasDir = path.join(nasBaseDir, 'auto-default');
301
302 const localNasDir = path.join(nasDir, serviceName);
303
304 if (!(await fs.pathExists(localNasDir))) {
305 await fs.ensureDir(localNasDir);
306 }
307
308 return [{
309 localNasDir,
310 remoteNasDir: '/mnt/auto'
311 }];
312 }
313 const mountPoints = nasConfig.MountPoints;
314
315 return await convertMountPointsToNasMappings(nasBaseDir, mountPoints);
316}
317
318async function convertTplToServiceNasMappings(nasBaseDir, tpl) {
319 const serviceNasMappings = {};
320
321 for (const { serviceName, serviceRes } of definition.findServices(tpl.Resources)) {
322 const nasConfig = (serviceRes.Properties || {}).NasConfig;
323
324 const nasMappings = await convertNasConfigToNasMappings(nasBaseDir, nasConfig, serviceName);
325
326 serviceNasMappings[serviceName] = nasMappings;
327 }
328
329 return serviceNasMappings;
330}
331
332function convertTplToServiceNasIdMappings(tpl) {
333 const serviceNasIdMappings = {};
334
335 for (const { serviceName, serviceRes } of definition.findServices(tpl.Resources)) {
336 const nasConfig = (serviceRes.Properties || {}).NasConfig;
337 var nasId;
338 if (nasConfig === undefined) {
339 nasId = {};
340 } else {
341 nasId = getNasIdFromNasConfig(nasConfig);
342 }
343
344 serviceNasIdMappings[serviceName] = nasId;
345 }
346
347 return serviceNasIdMappings;
348}
349
350function getNasIdFromNasConfig(nasConfig) {
351 const { userId, groupId } = definition.getUserIdAndGroupId(nasConfig);
352 return {
353 UserId: userId,
354 GroupId: groupId
355 };
356}
357
358function getDefaultNasDir(baseDir) {
359 return path.join(baseDir, tpl.DEFAULT_NAS_PATH_SUFFIX);
360}
361
362async function getNasFileSystems(nasClient, region) {
363 const pageSize = 50;
364 let requestPageNumber = 0;
365 let totalCount;
366 let pageNumber;
367
368 let fileSystems = [];
369
370 do {
371 const params = {
372 'RegionId': region,
373 'PageSize': pageSize,
374 'PageNumber': ++requestPageNumber
375 };
376
377 var rs;
378 try {
379 rs = await nasClient.request('DescribeFileSystems', params, requestOption);
380 } catch (ex) {
381 throwProcessedException(ex, 'AliyunNASFullAccess');
382 }
383
384 totalCount = rs.TotalCount;
385 pageNumber = rs.PageNumber;
386
387 fileSystems.push((rs.FileSystems || {}).FileSystem);
388
389 debug('find fileSystems: ' + JSON.stringify(fileSystems));
390
391 } while (totalCount && pageNumber && pageNumber * pageSize < totalCount);
392
393 return fileSystems;
394}
395
396const FAST_NAS_STORAGE_TYPE = ['standard', 'advance'];
397
398async function getAvailableNasFileSystems(nasClient) {
399 const profile = await getProfile();
400 const fileSystems = await getNasFileSystems(nasClient, profile.defaultRegion);
401 return _.flatten(fileSystems)
402 .reduce((acc, cur) => {
403
404 if ((cur.Description || '').indexOf('cloudshell') !== -1 || _.includes(FAST_NAS_STORAGE_TYPE, cur.StorageType)) {
405 return acc;
406 }
407 const mountTargets = cur.MountTargets || {};
408
409 const availableMounts = [];
410 for (const m of mountTargets.MountTarget) {
411 if (m.Status === 'active' && m.NetworkType === 'vpc') { // 可用的, 非经典网络
412 availableMounts.push(m);
413 }
414 }
415 acc.push({
416 fileSystemId: cur.FileSystemId,
417 description: cur.Description,
418 storageType: cur.StorageType,
419 zoneId: cur.ZoneId,
420 mountTargets: availableMounts
421 });
422 return acc;
423 }, []);
424}
425
426async function describeNasZones(nasClient, region) {
427 const params = {
428 'RegionId': region
429 };
430
431 const zones = await nasClient.request('DescribeZones', params, requestOption);
432 return zones.Zones.Zone;
433}
434
435module.exports = {
436 findNasFileSystem,
437 findMountTarget,
438 createMountTarget,
439 generateAutoNasConfig,
440 resolveMountPoint,
441 convertMountPointToNasMapping,
442 convertNasConfigToNasMappings,
443 convertTplToServiceNasMappings,
444 convertTplToServiceNasIdMappings,
445 getNasIdFromNasConfig,
446 getDefaultNasDir,
447 getAvailableNasFileSystems,
448 describeNasZones
449};
\No newline at end of file