아키텍처와 함께

블로그 이미지
by gregorio
  • Total hit
  • Today hit
  • Yesterday hit

'전체 글'에 해당되는 글 56건

  1. 2018.07.11
    Properties File을 이용한 Logback 설정
  2. 2018.07.06
    Java를 이용한 서버 CPU, 메모리 자원 모니터링
  3. 2018.07.05
    Ant script for Java project build
  4. 2018.07.05
    ant script for zip
  5. 2018.07.05
    Java에서 Shell호출 방법

응용 시스템에서 로그를 남기기 위해 Logback을 사용하는 경우 Properties를 별도의 파일로 분리하여 설정이 가능하다.


■ logback.properties


#######################################################

# Logback propertiies                                  #

#######################################################

log.dir=d:/temp/logs/batchagent

log.level=DEBUG

app.log.name=agent_app

err.log.name=agent_error

max.history=7

max.log.file.size=50MB



Properties 파일은 key=value로 정의하면 된다.



■ logback.xml


<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE xml>

<configuration scan="true" scanPeriod="60 seconds">


<property file="${user.dir}/conf/logback.properties" />


<!--  Application Log Appender -->

<appender name="applog" class="ch.qos.logback.core.rolling.RollingFileAppender">

<file>${log.dir}/${app.log.name}.log</file>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${log.dir}/%d{yyyy-MM, aux}/${app.log.name}.%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">

<maxFileSize>${max.log.file.size}</maxFileSize>

</timeBasedFileNamingAndTriggeringPolicy>

<maxHistory>${max.history}</maxHistory>

</rollingPolicy>


<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">

<charset>UTF-8</charset>

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50}.%M\(%line\) | %msg%n</pattern>

</encoder>

<filter class="ch.qos.logback.classic.filter.ThresholdFilter">

<level>${log.level}</level>

</filter>

</appender>



<!-- Error Log Appender -->

<appender name="errorlog" class="ch.qos.logback.core.rolling.RollingFileAppender">

<file>${log.dir}/${err.log.name}.log</file>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${log.dir}/%d{yyyy-MM, aux}/${err.log.name}.%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">

<maxFileSize>${max.log.file.size}</maxFileSize>

</timeBasedFileNamingAndTriggeringPolicy>

<maxHistory>${max.history}</maxHistory>

</rollingPolicy>


<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">

<charset>UTF-8</charset>

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50}.%M\(%line\) | %msg%n</pattern>

</encoder>

<filter class="ch.qos.logback.classic.filter.ThresholdFilter">

<level>WARN</level>

        </filter>

</appender>

<!-- SQL Log Appender -->

<appender name="sqllog" class="ch.qos.logback.core.rolling.RollingFileAppender">

<file>${LOG_DIR}/${SQL_LOG}.log</file>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${log.dir}/%d{yyyy-MM, aux}/${sql.log.name}.%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">

<maxFileSize>${max.log.file.size}</maxFileSize>

</timeBasedFileNamingAndTriggeringPolicy>

<maxHistory>${max.history}</maxHistory>

</rollingPolicy>


<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">

<charset>UTF-8</charset>

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50}.%M\(%line\) | %msg%n</pattern>

</encoder>

</appender>

<!-- End log appender -->


<logger name="org.springframework" level="${log.level}" additivity="false">

<appender-ref ref="applog" />

<appender-ref ref="errorlog" />

</logger>


<logger name="org.springframework.jdbc" additivity="false">

        <level value="WARN" />

<appender-ref ref="errorlog" />

    </logger>


<logger name="org.mybatis.spring" level="${log.level}" additivity="false">

<appender-ref ref="applog" />

<appender-ref ref="errorlog" />

</logger>


<logger name="io.netty" level="${log.level}" additivity="false">

<appender-ref ref="applog" />

<appender-ref ref="errorlog" />

</logger>

<logger name="org.apache.http" level="${log.level}" additivity="false">

<appender-ref ref="applog" />

<appender-ref ref="errorlog" />

</logger>


<logger name="org.apache.ibatis" level="${log.level}" additivity="false">

<appender-ref ref="applog" />

<appender-ref ref="errorlog" />

</logger>


<logger name="org.quartz" level="${log.level}" additivity="false">

<appender-ref ref="applog" />

<appender-ref ref="errorlog" />

</logger>


<!-- SQL base level -->

<logger name="java.sql" additivity="false">

<level value="WARN" />

<appender-ref ref="errorlog" />

</logger>


<!--JDBC SQL Query Print logging level-->

<logger name="jdbc.connection" level="OFF" additivity="false"> 

<appender-ref ref="errorlog" />

</logger>


    <logger name="jdbc.resultset" level="OFF" additivity="false"> 

<appender-ref ref="errorlog" />

    </logger>


<logger name="jdbc.audit" level="OFF" additivity="false"> 

<appender-ref ref="errorlog" />

</logger>


<logger name="jdbc.sqltiming" level="OFF" additivity="false"> 

<appender-ref ref="sqllog" />

    </logger>


<!-- want to log sql result, set level info  -->

<logger name="jdbc.resultsettable" level="INFO" additivity="false"> 

<appender-ref ref="sqllog" />

</logger>

<logger name="jdbc.sqlonly" level="DEBUG" additivity="false">

<appender-ref ref="sqllog" />

</logger>


<root level="${log.level}">

<appender-ref ref="console" />

<appender-ref ref="errorlog" />

</root>

</configuration>

 

logback.xml 파일에 properties 파일을 <property file="" />를 이용하여 Properties 파일을 읽는다.


Properties 파일에 정의되어 있는 Property를 사용할 때 ${property key}로 사용하면 된다.

AND

Java를 이용하여 시스템에 대한 CPU, 메모리의 정보를 받아오기 위해서 sigar Library를 이용하면 쉽고 정확하게 원하는 정보를 추출할 수 있다.


sigar는 scouter APM 툴에서 서버의 자원을 모니터링하기 위해서 사용하고 있다.


■ 소스 예제


import java.util.HashMap;

import java.util.Map;


import org.hyperic.sigar.CpuPerc;

import org.hyperic.sigar.DirStat;

import org.hyperic.sigar.DiskUsage;

import org.hyperic.sigar.FileSystem;

import org.hyperic.sigar.FileSystemUsage;

import org.hyperic.sigar.Mem;

import org.hyperic.sigar.NetFlags;

import org.hyperic.sigar.NetInterfaceConfig;

import org.hyperic.sigar.NetInterfaceStat;

import org.hyperic.sigar.Sigar;

import org.hyperic.sigar.SigarProxy;

import org.hyperic.sigar.SigarProxyCache;

import org.hyperic.sigar.Swap;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;


public class HostPerf {


private static final Logger LOGGER = LoggerFactory.getLogger(HostPerf.class);


static int SLEEP_TIME = 2000;

static Sigar sigarImpl = new Sigar();

static SigarProxy sigar = SigarProxyCache.newInstance(sigarImpl, SLEEP_TIME);


public static Map<String, Float> getCpuUsage() throws Exception {

Map<String, Float> cpuInfo = new HashMap<String, Float>();

CpuPerc cpuPerc = sigar.getCpuPerc();

float idle = (float) cpuPerc.getIdle();

float cpu = (float) ((1.0D - idle) * 100.0D);

float sysCpu = (float) cpuPerc.getSys() * 100.0F;

float userCpu = (float) cpuPerc.getUser() * 100.0F;


cpuInfo.put("cpuUsed", cpu);

cpuInfo.put("cpuFree", idle);

cpuInfo.put("sysCpu", sysCpu);

cpuInfo.put("userCpu", userCpu);


return cpuInfo;

}


public static Map<String, Float> getMemInfo() throws Exception {

Map<String, Float> memInfo = new HashMap<String, Float>();

Mem m = sigar.getMem();

float tmem = m.getTotal();

float fmem = m.getActualFree();

float umem = m.getActualUsed();

float memrate = (float) m.getUsedPercent();

memInfo.put("memTotal", tmem);

memInfo.put("memFree", fmem);

memInfo.put("memUsed", umem);

memInfo.put("memRate", memrate);


Swap sw = sigar.getSwap();


float pagein = sw.getPageIn();

float pageout = sw.getPageOut();

float tswap = sw.getTotal();

float uswap = sw.getUsed();

float swaprate = tswap == 0L ? 0.0F : (float) uswap * 100.0F / (float) tswap;


memInfo.put("pageIn", pagein);

memInfo.put("pageOut", pageout);

memInfo.put("swapTotal", tswap);

memInfo.put("swapUsed", uswap);

memInfo.put("swapRate", swaprate);


return memInfo;

}

}


소스코드를 수행하면 다음과 같은 오류메세지가 발생한다.


■ 오류 


no sigar-amd64-winnt.dll in java.library.path

org.hyperic.sigar.SigarException: no sigar-amd64-winnt.dll in java.library.path 

위의 오류를 해결하기 위해 sigar에서 제공하는 OS 환경에 맞는 Library를 설정해야한다.


먼저 hyperic-sigar-1.6.4.zip를 다운로드하여 압축을 해제한 후 Runtime 환경에 맞는 Library를 디렉토리에 Copy 한다.


프로그램을 실행할 때 JVM 파라미터로 -Djava.library.path=Library Directory 입력한 후 프로그램을 실행한다.


■ 실행결과


 


{cpuFree=0.92296225, cpuUsed=7.7037754, userCpu=1.4910537, sysCpu=5.417495}

{memFree=6.194135E8, swapUsed=6.4725484E9, memTotal=4.19631923E9, swapTotal=8.3907338E9, pageIn=2.0350976E8, memUsed=3.57690573E9, pageOut=1691194.0, swapRate=77.139244, memRate=85.23912}






AND

일반적인 Java project를 빌드하여 개발환경에 배포하는 Ant Script이다.


■ Ant script 예제

<?xml version="1.0" encoding="UTF-8"?>


<project name="portal" default="default" basedir=".">


    <!-- CI Informaton for build   -->

    <property name="ci.home"           value="/engn001/ciserv" />

<property name="ci.job.home"       value="${ci.home}/hudson/jobs" />

<property name="ci.app.home"       value="${ci.job.home}/HUDSON-JOB-NAME/workspace" />


<-- Project informamtion -->

    <property name="app.name" value="xxxx" />

    <property name="java.encoding" value="UTF-8" />

<property name="src.dir"  value="src/main/java" />


    <!-- Copy source to tempoary directory -->

<property name="tmp.dir" value="${ci.app.home}/target/${app.name}"/>

    <property name="tmp.dir.classes" value="${tmp.dir}/WEB-INF/classes"/>


    <property name="lib.dir" value="${tmp.dir}/WEB-INF/lib"/>

<!-- Server information for development -->

<property name="was.ip" value=""/>

<property name="was.port" value="" /> 

<property name="was.userid" value="" />

<property name="keyfile.dir" value="" />

<property name="was.home" value="/engn001/apache-tomcat-7.0.76" />

<property name="ci.war.dir" value="/engn001/ciserv/hudson/jobs/HUDSON-JOB-NAME/workspace" />

<property name="war.filename" value="portal.war" />

<!-- Source directory for development -->

<property name="was.tmp.src.dir" value="" /> 

<property name="was.src.dir" value="" />


<!-- =============================================================== -->

<!-- Build source to war  -->

<!-- =============================================================== -->

<path id="classpath">

<fileset dir="${lib.dir}" includes="**/*.jar"/>

<fileset dir="/engn001/ciserv/lib/tomcat7" includes="**/*.jar"/>

</path>


<!-- Start Build -->

    <target name="default" depends="init, prepare, compile, war, stop-tomcat, copy-war, unzip-war, start-tomcat"/>

<!-- Tempoary directory delete and create -->

    <target name="init">

    <delete dir="${tmp.dir.classes}"/>

    <mkdir  dir="${tmp.dir}" />

    <mkdir  dir="${tmp.dir.classes}" />

    </target>


<!-- Synchronize source to tempoary directory -->

    <target name="prepare" depends="init">

    <sync todir="${tmp.dir}">

<fileset dir="${src.dir.web}"/>

</sync>

    <sync todir="${tmp.dir.classes}">

<fileset dir="${src.resources.dir}"/>

</sync>


    </target>


<!-- Source compile for portal development -->

    <target name="compile" depends="prepare">

    <javac srcdir="${src.dir}" destdir="${tmp.dir.classes}" deprecation="off" debug="on" includeantruntime="true">

<classpath refid="classpath"/>

</javac>

</target>


<!-- Generate war file for portal development -->

    <target name="war" depends="compile">

<jar jarfile="${app.name}.war">

<fileset dir="${tmp.dir}">

<include name="**/*"/>

<exclude name="demo/**"/>

<exclude name="help/**"/>

<exclude name="report/**"/>

<exclude name="resource/**"/>

<exclude name="xml/**" />

</fileset>

</jar>

</target>

    <!-- Copy war file to portal development server in /tmp/src directory-->

<target name="copy-war" depends="war">

<echo message="Remote copy war file to was server "/>

<exec executable="scp" dir="." failonerror="true">

    <arg value="-r"/>

    <arg value="-i"/>

    <arg value="${keyfile.dir}" />

    <arg value="${ci.war.dir}/${war.filename}" />

    <arg value="${was.userid}@${was.ip}:${was.tmp.src.dir}"/>

    </exec>

</target>

<!--Stop tomcat -->

<target name="stop-tomcat" depends="copy-war">

<sshexec  host="${was.ip}"

username="${was.userid}"

port="${was.port}"

keyfile="${keyfile.dir}"

command="sudo su -c ${was.home}/bin/shutdown.sh tomcat;

cd ${was.src.dir};

sudo rm -rf *"

trust="true"/>

</target>

<sleep seconds="10" />

<!-- Unzip war file in portal development source directory -->

<target name="unzip-war" depends="stop-tomcat">

<echo message="WAS server source unzip" />

<sshexec  host="${was.ip}"

username="${was.userid}"

keyfile="${keyfile.dir}"

port="${was.port}"

command="sudo cp ${was.tmp.src.dir}/${war.filename} ${was.src.dir};

cd ${was.src.dir};

sudo unzip ${was.src.dir}/${war.filename};

sudo cp ${was.src.dir}/WEB-INF/classes/svrcfg/dev/bizactor.controlling.jar ${was.src.dir}/WEB-INF/lib/bizactor.controlling.jar;

sudo cp ${was.src.dir}/WEB-INF/classes/svrcfg/dev/bizactor.modeling.jar ${was.src.dir}/WEB-INF/lib/bizactor.modeling.jar;

sudo rm -rf ${was.src.dir}/${app.name}.war;

sudo chown -R tomcat:tomcat /logs001/ibm;

sudo chown -R tomcat:tomcat *"

trust="true"/>

<echo message="Web server source unzip" />

</target>

<!-- Start portal development tomcat -->

<echo message="Tomcat Start" />

<target name="start-tomcat" depends="unzip-war">

<sshexec  host="${was.ip}"

username="${was.userid}"

keyfile="${keyfile.dir}"

port="${was.port}"

command="cd ${was.src.dir}

sudo chown -R tomcat:tomcat *;

sudo su -c ${was.home}/bin/startup.sh tomcat"

trust="true"/>

</target>

</project>


AND

Java Project가 아닌 단순 Project를 생성하여 ZIP으로 압축한 후 배포 서버로 생성된 ZIP 파일을 전송한 후 압축을 해제하는 Ant Build Script이다.


■ Project ZIP Ant build script

<?xml version="1.0" encoding="UTF-8"?>

<!--

To change this license header, choose License Headers in Project Properties.

To change this template file, choose Tools | Templates

and open the template in the editor.

-->

<project name="sehati-oz" default="main" basedir=".">

    <description>

        Description of your project

    </description>

 

    <property name="ci.home" value="/engn001/ciserv" />

<property name="ci.job.home" value="${ci.home}/hudson/jobs" />

<property name="ci.app.home" value="${ci.job.home}/SEHATI-OZ-DEV/workspace" />

<property name="projectName" value="xxxx-oz" />

<!-- OZ sources -->

<property name="oz.dir" location="oz" />  

<property name="target.dir" value="${ci.app.home}/target" />

    <property name="portal.ip" value=""/>

<property name="ibm.ip" value="" />

<property name="was.port" value="" />

<property name="was.userid" value="" />

<property name="keyfile.dir" value="" />

<property name="was.tmp.src.dir" value="/tmp/src" /> 

<property name="aaa.oz.target" value="" />

<property name="bbb.oz.target" value="" />


<target name="main" depends="clean, init, zip, unzip, systemzip, deleteDir, copyTarget, unzip-oz" />


<target name="clean" description="Flush staging directory">

    <delete dir="${staging.dir}" failonerror="false"/>   

</target>


<target name="init">

    <mkdir dir="${target.dir}" />

</target>

 

<target name="zip" depends="clean" description="package, output to ZIP">

    <zip destfile="${target.dir}/${projectName}.zip" basedir="${oz.dir}" />

</target>


<target name="unzip" depends="zip" description="package, output to UNZIP">

    <unzip src="${target.dir}/${projectName}.zip" dest="${target.dir}" />

</target>

<!-- zip file according to system -->

<target name="systemzip" depends="unzip" description="package, output to ZIP">

    <zip destfile="${target.dir}/${projectName}_IBM.zip" basedir="${target.dir}/IBM" />

    <zip destfile="${target.dir}/${projectName}_PORTAL.zip" basedir="${target.dir}/PORTAL" />

</target>

<!-- CI Oz unzip dir delete -->

<target name="deleteDir" depends="unzip" description="package, output to ZIP">

    <delete dir="${target.dir}/IBM" />

    <delete dir="${target.dir}/PORTAL" />

</target>

<!--Copy to IBM, Porta WAS -->

<target name="copyTarget" depends="deleteDir" description="package, output to ZIP">

<exec executable="scp" dir="." failonerror="true">

    <arg value="-r"/>

    <arg value="-i"/>

    <arg value="${keyfile.dir}" />

    <arg value="${target.dir}/${projectName}_IBM.zip" />

    <arg value="${was.userid}@${ibm.ip}:${was.tmp.src.dir}"/>

    </exec>

<exec executable="scp" dir="." failonerror="true">

    <arg value="-r"/>

    <arg value="-i"/>

    <arg value="${keyfile.dir}" />

    <arg value="${target.dir}/${projectName}_PORTAL.zip" />

    <arg value="${was.userid}@${portal.ip}:${was.tmp.src.dir}"/>

    </exec>

</target>

<!--unzip -->

<target name="unzip-oz" depends="copyTarget">

<sshexec  host="${ibm.ip}"

username="${was.userid}"

keyfile="${keyfile.dir}"

port="${was.port}"

command="sudo cp ${was.tmp.src.dir}/${projectName}_IBM.zip ${ibm.oz.target};

cd ${ibm.oz.target};

sudo unzip -o ${projectName}_IBM.zip;

sudo rm -rf ${projectName}_IBM.zip;

sudo chown tomcat:tomcat ${ibm.oz.target}"

trust="true"/>

<sshexec  host="${portal.ip}"

username="${was.userid}"

keyfile="${keyfile.dir}"

port="${was.port}"

command="sudo cp ${was.tmp.src.dir}/${projectName}_PORTAL.zip ${portal.oz.target};

cd ${portal.oz.target};

sudo unzip -o ${projectName}_PORTAL.zip;

sudo rm -rf ${projectName}_PORTAL.zip;

sudo chown tomcat:tomcat ${portal.oz.target}"

trust="true"/>

</target>

</project> 


AND

Java에서 Shell 호출 시 apache commons-exec를 사용하면 쉽게 shell을 호출하고 shell에 파라미터 전달도 가능하다.


■ 소스

import java.io.ByteArrayOutputStream;

import java.util.HashMap;

import java.util.Map;


import org.apache.commons.exec.CommandLine;

import org.apache.commons.exec.DefaultExecuteResultHandler;

import org.apache.commons.exec.DefaultExecutor;

import org.apache.commons.exec.ExecuteStreamHandler;

import org.apache.commons.exec.ExecuteWatchdog;

import org.apache.commons.exec.PumpStreamHandler;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.servlet.ModelAndView;


import ncd.spring.common.util.PropertyUtil;


@Controller

public class DeployController {


private static final Logger LOGGER = LoggerFactory.getLogger(DeployController.class);

@RequestMapping(value = "/admin/deploy.do")

public ModelAndView sourceDeploy(@RequestParam("type") String type) throws Exception {

LOGGER.info("Start Source Deploy....");

final Map<String, Object> resultMap = new HashMap<String, Object>();


String scriptFile  = PropertyUtil.getString("deploy.script.file");

String confFile = null;

if ("oz".equals(type)) {

confFile = PropertyUtil.getString("oz.deploy.config.file");

}

else {

confFile = PropertyUtil.getString("was.deploy.config.file");

}

if (scriptFile == null || confFile == null) {

resultMap.put("error", "script file or config file is null");

return new ModelAndView("ajaxMultiDataView", resultMap);

}

LOGGER.info("{},{}", scriptFile, confFile);

/** Initiate Executor **/

DefaultExecutor executor = new DefaultExecutor();

ExecuteWatchdog watchDog = new ExecuteWatchdog(60*1000);

executor.setWatchdog(watchDog);

DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();

ByteArrayOutputStream bos = new ByteArrayOutputStream();

ExecuteStreamHandler stream = new PumpStreamHandler(bos, bos, null);

executor.setStreamHandler(stream);

/** Initiate Command Line **/

CommandLine cmdLine = new CommandLine(PropertyUtil.getString("deploy.script.command"));

/** Set parameter to command line **/

cmdLine.addArgument(scriptFile);

cmdLine.addArgument(confFile);

cmdLine.addArgument(type);

LOGGER.info("Execute Command :: ", cmdLine.toString());

/** Execute shell **/

try {

executor.execute(cmdLine, resultHandler);

resultHandler.waitFor();

int exitCode = resultHandler.getExitValue();

String result = bos.toString();

LOGGER.info("Result :: ", result);

resultMap.put("result", result);

resultMap.put("exitCode", exitCode);

}

catch(Exception ex) {

LOGGER.error(ex.getMessage());

}

finally {

bos.close();

}

ModelAndView mav = new ModelAndView("ajaxMultiDataView", resultMap);

return mav;

}



Shell의 수행결과를 받기 위해서는 PumpStreamHandeler를 생성하고, executer에 StreamHanlder에 Set한다.



'Java' 카테고리의 다른 글

Java Convert Map to XML 예제  (0) 2018.07.26
JVM GC 설정  (0) 2018.06.07
Java Reflection 사용법  (0) 2018.04.12
JSP의 img, script에 jsessionid가 추가되는 경우  (0) 2018.02.22
Html a tag function call  (0) 2018.02.20
AND

ARTICLE CATEGORY

분류 전체보기 (56)
Spring Framrwork (33)
Linux (1)
APM (1)
Java (8)
python (0)
ant (1)
chart (1)
OS (1)
tomcat (1)
apache (1)
database (0)

RECENT ARTICLE

RECENT COMMENT

CALENDAR

«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31

ARCHIVE

LINK